diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bafc3aa60..1160f02c1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,9 +17,9 @@ jobs: cache: 'gradle' distribution: 'adopt' - name: Set up server - run: ./gradlew -PskipBuild=zombies-mapeditor :phantazm-server:setupServer + run: ./gradlew -PskipBuild=zombies-mapeditor,zombies-timer :phantazm-server:setupServer - name: Build & copy jar - run: ./gradlew -PskipBuild=zombies-mapeditor :phantazm-server:copyJar + run: ./gradlew -PskipBuild=zombies-mapeditor,zombies-timer :phantazm-server:copyJar - name: Zip server files run: | cd ./run/server-1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9eb1cce62..e98bdf0fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,22 +1,24 @@ name: Test PhantazmServer on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + push: + branches: [ main ] + pull_request: + branches: [ main ] jobs: - build: - runs-on: ubuntu-latest + build: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - cache: 'gradle' - distribution: 'adopt' - - name: Run tests - run: ./gradlew test + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + cache: 'gradle' + distribution: 'adopt' + - name: Run tests + run: ./gradlew test diff --git a/.run/Run Velocity.run.xml b/.run/Run Velocity.run.xml index cbb04a6e3..f3c46f1ae 100644 --- a/.run/Run Velocity.run.xml +++ b/.run/Run Velocity.run.xml @@ -1,15 +1,12 @@ - - - + + + \ No newline at end of file diff --git a/.run/Run server.run.xml b/.run/Run server.run.xml index e6b3ba9c2..694ab74e5 100644 --- a/.run/Run server.run.xml +++ b/.run/Run server.run.xml @@ -1,12 +1,12 @@ \ No newline at end of file diff --git a/commons/src/main/java/org/phantazm/commons/MathUtils.java b/commons/src/main/java/org/phantazm/commons/MathUtils.java index 5a0881b88..f05727765 100644 --- a/commons/src/main/java/org/phantazm/commons/MathUtils.java +++ b/commons/src/main/java/org/phantazm/commons/MathUtils.java @@ -12,6 +12,10 @@ private MathUtils() { throw new UnsupportedOperationException(); } + public static boolean fuzzyEquals(double a, double b, double epsilon) { + return Math.abs(a - b) < epsilon; + } + public static long randomInterval(long min, long max) { return (long)(Math.random() * (min - max)) + min; } diff --git a/commons/src/main/java/org/phantazm/commons/TickableTask.java b/commons/src/main/java/org/phantazm/commons/TickableTask.java index 419265ab4..ab58a2afa 100644 --- a/commons/src/main/java/org/phantazm/commons/TickableTask.java +++ b/commons/src/main/java/org/phantazm/commons/TickableTask.java @@ -1,9 +1,38 @@ package org.phantazm.commons; +import org.jetbrains.annotations.NotNull; + public interface TickableTask extends Tickable { boolean isFinished(); default void end() { } + + static @NotNull TickableTask afterTicks(long ticks, @NotNull Runnable action, @NotNull Runnable endAction) { + long start = System.currentTimeMillis(); + return new TickableTask() { + private boolean finished; + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public void tick(long time) { + if ((time - start) / 50 >= ticks) { + finished = true; + action.run(); + } + } + + @Override + public void end() { + if (!finished) { + endAction.run(); + } + } + }; + } } diff --git a/commons/src/main/java/org/phantazm/commons/chat/ChatDestination.java b/commons/src/main/java/org/phantazm/commons/chat/ChatDestination.java new file mode 100644 index 000000000..f291a223e --- /dev/null +++ b/commons/src/main/java/org/phantazm/commons/chat/ChatDestination.java @@ -0,0 +1,8 @@ +package org.phantazm.commons.chat; + +public enum ChatDestination { + TITLE, + SUBTITLE, + CHAT, + ACTION_BAR +} diff --git a/commons/src/main/java/org/phantazm/commons/chat/MessageWithDestination.java b/commons/src/main/java/org/phantazm/commons/chat/MessageWithDestination.java new file mode 100644 index 000000000..bb45a2083 --- /dev/null +++ b/commons/src/main/java/org/phantazm/commons/chat/MessageWithDestination.java @@ -0,0 +1,8 @@ +package org.phantazm.commons.chat; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +public record MessageWithDestination(@NotNull Component component, @NotNull ChatDestination destination) { + +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d4506a242..03247f58b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { api(projects.phantazmCommons) + api(projects.phantazmMessaging) api(libs.ethylene.core) api(libs.ethylene.mapper) api(libs.commons.lang3) diff --git a/core/src/main/java/org/phantazm/core/BasicClientBlockHandlerSource.java b/core/src/main/java/org/phantazm/core/BasicClientBlockHandlerSource.java index 37f048efb..7564772ad 100644 --- a/core/src/main/java/org/phantazm/core/BasicClientBlockHandlerSource.java +++ b/core/src/main/java/org/phantazm/core/BasicClientBlockHandlerSource.java @@ -1,35 +1,32 @@ package org.phantazm.core; import net.minestom.server.event.Event; +import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.InstanceUnregisterEvent; +import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; +import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; /** * Standard implementation of {@link ClientBlockHandlerSource}. */ public class BasicClientBlockHandlerSource implements ClientBlockHandlerSource { - private final Function blockHandlerFunction; - private final Map map; - - /** - * Creates a new instance of this class given the provided {@link ClientBlockHandler}-producing function. This - * function must never return null. - * - * @param handlerFunction the handler function, which should never return null - */ - public BasicClientBlockHandlerSource( - @NotNull Function handlerFunction, - @NotNull EventNode globalNode) { - this.blockHandlerFunction = Objects.requireNonNull(handlerFunction, "handlerFunction"); + private final EventNode rootNode; + private final Map map; + + private record Node(@NotNull ClientBlockHandler handler, @NotNull EventNode node) { + } + + public BasicClientBlockHandlerSource(@NotNull EventNode rootNode) { + this.rootNode = rootNode; this.map = new ConcurrentHashMap<>(); - globalNode.addListener(InstanceUnregisterEvent.class, this::onInstanceUnregister); + rootNode.addListener(InstanceUnregisterEvent.class, this::onInstanceUnregister); } @Override @@ -39,11 +36,24 @@ public BasicClientBlockHandlerSource( throw new IllegalArgumentException("Cannot hold an unregistered instance"); } - return Objects.requireNonNull(this.blockHandlerFunction.apply(instance), "handler"); - }); + EventNode instanceNode = + EventNode.type("client_block_handler_" + instance.getUniqueId(), EventFilter.INSTANCE, + (e, v) -> v == instance); + DimensionType type = instance.getDimensionType(); + Node node = + new Node(new InstanceClientBlockHandler(instance, type.getMinY(), type.getHeight(), instanceNode), + instanceNode); + + rootNode.addChild(instanceNode); + + return node; + }).handler; } private void onInstanceUnregister(InstanceUnregisterEvent event) { - map.remove(event.getInstance().getUniqueId()); + Node node = map.remove(event.getInstance().getUniqueId()); + if (node != null) { + rootNode.removeChild(node.node); + } } } diff --git a/core/src/main/java/org/phantazm/core/ComponentUtils.java b/core/src/main/java/org/phantazm/core/ComponentUtils.java deleted file mode 100644 index 18a136183..000000000 --- a/core/src/main/java/org/phantazm/core/ComponentUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.phantazm.core; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.jetbrains.annotations.NotNull; - -import java.util.IllegalFormatException; -import java.util.Objects; - -public final class ComponentUtils { - private ComponentUtils() { - } - - public static @NotNull Component tryFormat(@NotNull String formatString, Object... objects) { - Objects.requireNonNull(formatString, "formatString"); - - String message; - try { - message = String.format(formatString, objects); - } - catch (IllegalFormatException ignored) { - message = formatString; - } - - return MiniMessage.miniMessage().deserialize(message); - } -} diff --git a/core/src/main/java/org/phantazm/core/InstanceClientBlockHandler.java b/core/src/main/java/org/phantazm/core/InstanceClientBlockHandler.java index 28d6a73eb..3a0a7f6df 100644 --- a/core/src/main/java/org/phantazm/core/InstanceClientBlockHandler.java +++ b/core/src/main/java/org/phantazm/core/InstanceClientBlockHandler.java @@ -44,18 +44,18 @@ public class InstanceClientBlockHandler implements ClientBlockHandler { * @param instance the instance this handler is bound to * @param chunkFloor the minimum y-coordinate of chunks in this instance */ - public InstanceClientBlockHandler(@NotNull Instance instance, int chunkFloor, int chunkHeight) { + public InstanceClientBlockHandler(@NotNull Instance instance, int chunkFloor, int chunkHeight, + @NotNull EventNode instanceNode) { this.instance = Objects.requireNonNull(instance, "instance"); this.clientData = new Long2ObjectOpenHashMap<>(); this.chunkFloor = chunkFloor; this.chunkHeight = chunkHeight; - EventNode node = instance.eventNode(); - node.addListener(PreBlockChangeEvent.class, this::onPreBlockChange); - node.addListener(PlayerBlockBreakEvent.class, this::onPlayerBlockBreak); - node.addListener(PrePlayerStartDiggingEvent.class, this::onPrePlayerStartDigging); - node.addListener(PreSendChunkEvent.class, this::onPreSendChunk); - node.addListener(InstanceChunkUnloadEvent.class, this::onChunkUnload); + instanceNode.addListener(PreBlockChangeEvent.class, this::onPreBlockChange); + instanceNode.addListener(PlayerBlockBreakEvent.class, this::onPlayerBlockBreak); + instanceNode.addListener(PrePlayerStartDiggingEvent.class, this::onPrePlayerStartDigging); + instanceNode.addListener(PreSendChunkEvent.class, this::onPreSendChunk); + instanceNode.addListener(InstanceChunkUnloadEvent.class, this::onChunkUnload); } @Override diff --git a/core/src/main/java/org/phantazm/core/ItemStackUtils.java b/core/src/main/java/org/phantazm/core/ItemStackUtils.java new file mode 100644 index 000000000..5b1daae24 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/ItemStackUtils.java @@ -0,0 +1,44 @@ +package org.phantazm.core; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; +import org.jglrxavpok.hephaistos.nbt.NBTException; +import org.jglrxavpok.hephaistos.parser.SNBTParser; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +public final class ItemStackUtils { + public static @NotNull ItemStack buildItem(@NotNull Material material, @Nullable String tag, + @Nullable String displayName, @Nullable List lore, @NotNull TagResolver @NotNull ... tags) { + ItemStack.Builder builder = ItemStack.builder(material); + if (tag != null) { + try { + builder.meta((NBTCompound)new SNBTParser(new StringReader(tag)).parse()); + } + catch (NBTException ignored) { + } + } + + if (displayName != null) { + builder.displayName(MiniMessage.miniMessage().deserialize(displayName, tags)); + } + + if (lore != null) { + List components = new ArrayList<>(lore.size()); + for (String format : lore) { + components.add(MiniMessage.miniMessage().deserialize(format, tags)); + } + builder.lore(components); + } + + return builder.build(); + } +} diff --git a/core/src/main/java/org/phantazm/core/Tags.java b/core/src/main/java/org/phantazm/core/Tags.java new file mode 100644 index 000000000..8a7e9f98e --- /dev/null +++ b/core/src/main/java/org/phantazm/core/Tags.java @@ -0,0 +1,9 @@ +package org.phantazm.core; + +import net.minestom.server.tag.Tag; + +import java.util.UUID; + +public final class Tags { + public static final Tag LAST_MELEE_HIT_TAG = Tag.UUID("last_melee_hit"); +} diff --git a/core/src/main/java/org/phantazm/core/chat/BasicChatChannel.java b/core/src/main/java/org/phantazm/core/chat/BasicChatChannel.java index 4d54b6d32..e13a2e128 100644 --- a/core/src/main/java/org/phantazm/core/chat/BasicChatChannel.java +++ b/core/src/main/java/org/phantazm/core/chat/BasicChatChannel.java @@ -5,6 +5,9 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; import org.jetbrains.annotations.NotNull; @@ -24,13 +27,20 @@ public abstract class BasicChatChannel implements ChatChannel { private final PlayerViewProvider viewProvider; + private final MiniMessage miniMessage; + + private final String chatFormat; + /** * Creates a {@link BasicChatChannel}. * * @param viewProvider The {@link BasicChatChannel}'s {@link PlayerViewProvider} */ - public BasicChatChannel(@NotNull PlayerViewProvider viewProvider) { + public BasicChatChannel(@NotNull PlayerViewProvider viewProvider, @NotNull MiniMessage miniMessage, + @NotNull String chatFormat) { this.viewProvider = Objects.requireNonNull(viewProvider, "viewProvider"); + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + this.chatFormat = Objects.requireNonNull(chatFormat, "chatFormat"); } @Override @@ -55,13 +65,17 @@ public void findAudience(@NotNull UUID channelMember, @NotNull Consumer chatFormatter = chatEvent.getChatFormatFunction(); - if (chatFormatter != null) { - return chatFormatter.apply(chatEvent); - } + public @NotNull Component formatMessage(@NotNull Player player, @NotNull String message) { + Optional displayNameOptional = + getViewProvider().fromPlayer(player).getDisplayNameIfCached(); + Component displayName = displayNameOptional.isPresent() + ? displayNameOptional.get() + : Component.text(player.getUsername()); - return chatEvent.getDefaultChatFormat().get(); + TagResolver senderPlaceholder = Placeholder.component("sender", displayName); + TagResolver messagePlaceholder = Placeholder.unparsed("message", message); + + return miniMessage.deserialize(chatFormat, senderPlaceholder, messagePlaceholder); } /** @@ -75,4 +89,7 @@ public void findAudience(@NotNull UUID channelMember, @NotNull Consumer> getAudience(@NotNull Player player); + protected @NotNull PlayerViewProvider getViewProvider() { + return viewProvider; + } } diff --git a/core/src/main/java/org/phantazm/core/chat/ChatChannel.java b/core/src/main/java/org/phantazm/core/chat/ChatChannel.java index ee7092920..a7cff0a48 100644 --- a/core/src/main/java/org/phantazm/core/chat/ChatChannel.java +++ b/core/src/main/java/org/phantazm/core/chat/ChatChannel.java @@ -3,6 +3,7 @@ import it.unimi.dsi.fastutil.objects.ObjectBooleanPair; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; import org.jetbrains.annotations.NotNull; @@ -28,9 +29,8 @@ void findAudience(@NotNull UUID channelMember, @NotNull Consumer onSuc /** * Formats a message. Channels may add custom style or formatting. * - * @param chatEvent The {@link PlayerChatEvent} that is being formatted * @return The formatted {@link Component} message */ - @NotNull Component formatMessage(@NotNull PlayerChatEvent chatEvent); + @NotNull Component formatMessage(@NotNull Player player, @NotNull String message); } diff --git a/core/src/main/java/org/phantazm/core/chat/ChatConfig.java b/core/src/main/java/org/phantazm/core/chat/ChatConfig.java new file mode 100644 index 000000000..a87f98c36 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/chat/ChatConfig.java @@ -0,0 +1,12 @@ +package org.phantazm.core.chat; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.guild.party.PartyChatChannel; + +import java.util.Map; + +public record ChatConfig(@NotNull Map chatFormats) { + + public static final ChatConfig DEFAULT = new ChatConfig(Map.of("all", "", PartyChatChannel.CHANNEL_NAME, "")); + +} diff --git a/core/src/main/java/org/phantazm/core/chat/InstanceChatChannel.java b/core/src/main/java/org/phantazm/core/chat/InstanceChatChannel.java index 569bfc10b..71e36e936 100644 --- a/core/src/main/java/org/phantazm/core/chat/InstanceChatChannel.java +++ b/core/src/main/java/org/phantazm/core/chat/InstanceChatChannel.java @@ -6,6 +6,7 @@ import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; @@ -23,8 +24,9 @@ public class InstanceChatChannel extends BasicChatChannel { * * @param viewProvider The {@link InstanceChatChannel}'s {@link PlayerViewProvider} */ - public InstanceChatChannel(@NotNull PlayerViewProvider viewProvider) { - super(viewProvider); + public InstanceChatChannel(@NotNull PlayerViewProvider viewProvider, @NotNull MiniMessage miniMessage, + @NotNull String chatFormat) { + super(viewProvider, miniMessage, chatFormat); } @Override diff --git a/core/src/main/java/org/phantazm/core/chat/SelfChatChannel.java b/core/src/main/java/org/phantazm/core/chat/SelfChatChannel.java deleted file mode 100644 index 55edbf3c4..000000000 --- a/core/src/main/java/org/phantazm/core/chat/SelfChatChannel.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.phantazm.core.chat; - -import it.unimi.dsi.fastutil.Pair; -import it.unimi.dsi.fastutil.objects.ObjectBooleanPair; -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.JoinConfiguration; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.entity.Player; -import net.minestom.server.event.player.PlayerChatEvent; -import org.jetbrains.annotations.NotNull; -import org.phantazm.core.player.PlayerViewProvider; - -/** - * A {@link ChatChannel} that only sends message to the message's sender. - */ -public class SelfChatChannel extends BasicChatChannel { - - public static final String CHANNEL_NAME = "self"; - - /** - * Creates a {@link SelfChatChannel}. - * - * @param viewProvider The {@link SelfChatChannel}'s {@link PlayerViewProvider} - */ - public SelfChatChannel(@NotNull PlayerViewProvider viewProvider) { - super(viewProvider); - } - - @Override - public @NotNull Component formatMessage(@NotNull PlayerChatEvent chatEvent) { - JoinConfiguration joinConfiguration = JoinConfiguration.separator(Component.space()); - return Component.join(joinConfiguration, Component.text("self"), Component.text(">", NamedTextColor.GRAY), - super.formatMessage(chatEvent)); - } - - @Override - protected @NotNull Pair> getAudience(@NotNull Player player) { - return Pair.of(player, null); - } -} diff --git a/core/src/main/java/org/phantazm/core/chat/command/ChatChannelSendCommand.java b/core/src/main/java/org/phantazm/core/chat/command/ChatChannelSendCommand.java new file mode 100644 index 000000000..a04756d96 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/chat/command/ChatChannelSendCommand.java @@ -0,0 +1,48 @@ +package org.phantazm.core.chat.command; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.chat.ChatChannel; + +import java.util.Objects; + +public class ChatChannelSendCommand { + + private ChatChannelSendCommand() { + throw new UnsupportedOperationException(); + } + + public static @NotNull Command chatChannelSend(@NotNull String commandName, @NotNull ChatChannel channel) { + Objects.requireNonNull(channel, "channel"); + Command command = new Command(commandName); + + Argument message = ArgumentType.StringArray("message"); + command.addConditionalSyntax((sender, commandString) -> { + if (commandString == null) { + return sender instanceof Player; + } + + if (!(sender instanceof Player)) { + sender.sendMessage(Component.text("You have to be a player to use that command!", NamedTextColor.RED)); + } + + return true; + }, (sender, context) -> { + Player player = (Player)sender; + channel.findAudience(player.getUuid(), audience -> { + Component formatted = channel.formatMessage(player, String.join(" ", context.get(message))); + audience.sendMessage(formatted); + }, (failure) -> { + player.sendMessage(failure.left()); + }); + }, message); + + return command; + } + +} diff --git a/core/src/main/java/org/phantazm/core/datapack/Datapack.java b/core/src/main/java/org/phantazm/core/datapack/Datapack.java new file mode 100644 index 000000000..d42ab692f --- /dev/null +++ b/core/src/main/java/org/phantazm/core/datapack/Datapack.java @@ -0,0 +1,11 @@ +package org.phantazm.core.datapack; + +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.world.biomes.Biome; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public record Datapack(@NotNull Map biomes) { + +} diff --git a/core/src/main/java/org/phantazm/core/datapack/DatapackLoader.java b/core/src/main/java/org/phantazm/core/datapack/DatapackLoader.java new file mode 100644 index 000000000..85be22309 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/datapack/DatapackLoader.java @@ -0,0 +1,127 @@ +package org.phantazm.core.datapack; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.world.biomes.Biome; +import net.minestom.server.world.biomes.BiomeEffects; +import net.minestom.server.world.biomes.BiomeParticle; +import org.jetbrains.annotations.NotNull; +import org.jglrxavpok.hephaistos.json.NBTGsonReader; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; +import org.jglrxavpok.hephaistos.nbt.NBTType; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.Map; +import java.util.TreeMap; + +public class DatapackLoader { + + public @NotNull Datapack loadDatapack(@NotNull Path datapackPath) throws IOException { + Path data = datapackPath.resolve("data"); + Map biomes = new TreeMap<>(); + try (DirectoryStream namespaceRoots = Files.newDirectoryStream(data, Files::isDirectory)) { + for (Path namespaceRoot : namespaceRoots) { + String namespace = namespaceRoot.getFileName().toString(); + biomes.putAll(loadBiomes(namespace, namespaceRoot)); + } + + return new Datapack(biomes); + } + } + + private Map loadBiomes(String namespace, Path root) throws IOException { + Map biomes = new TreeMap<>(); + Path biomeRoot = root.resolve("worldgen/biome"); + PathMatcher matcher = root.getFileSystem().getPathMatcher("glob:**/*.json"); + DirectoryStream.Filter filter = entry -> matcher.matches(entry) && Files.isRegularFile(entry); + try (DirectoryStream biomePaths = Files.newDirectoryStream(biomeRoot, filter)) { + for (Path biomePath : biomePaths) { + String fileName = biomePath.getFileName().toString(); + NamespaceID name = NamespaceID.from(namespace, fileName.substring(0, fileName.length() - ".json".length())); + Biome biome = loadBiome(name, biomePath); + biomes.put(name, biome); + } + } + + return biomes; + } + + private Biome loadBiome(NamespaceID name, Path path) throws IOException { + JsonObject rootJson = JsonParser.parseReader(Files.newBufferedReader(path)).getAsJsonObject(); + Biome.Builder biomeBuilder = Biome.builder() + .name(name) + .temperature(rootJson.get("temperature").getAsFloat()) + .downfall(rootJson.get("downfall").getAsFloat()) + .hasPrecipitation(rootJson.get("has_precipitation").getAsBoolean()); + if (rootJson.has("temperature_modifier")) { + biomeBuilder.temperatureModifier(Biome.TemperatureModifier.valueOf(rootJson.get("temperature_modifier").getAsString().toUpperCase())); + } + + JsonObject effectsJson = rootJson.getAsJsonObject("effects"); + BiomeEffects.Builder effectsBuilder = BiomeEffects.builder() + .fogColor(effectsJson.get("fog_color").getAsInt()) + .skyColor(effectsJson.get("sky_color").getAsInt()) + .waterColor(effectsJson.get("water_color").getAsInt()) + .waterFogColor(effectsJson.get("water_fog_color").getAsInt()); + if (effectsJson.has("foliage_color")) { + effectsBuilder.foliageColor(effectsJson.get("foliage_color").getAsInt()); + } + if (effectsJson.has("grass_color")) { + effectsBuilder.grassColor(effectsJson.get("grass_color").getAsInt()); + } + if (effectsJson.has("grass_color_modifier")) { + effectsBuilder.grassColorModifier(BiomeEffects.GrassColorModifier.valueOf(effectsJson.get( + "grass_color_modifier").getAsString().toUpperCase())); + } + if (effectsJson.has("particle")) { + JsonObject particleJson = effectsJson.getAsJsonObject("particle"); + float probability = particleJson.get("probability").getAsFloat(); + String particleSNBT = particleJson.get("options").toString(); + + try (NBTGsonReader nbtReader = new NBTGsonReader(new StringReader(particleSNBT))) { + NBTCompound particleNBT = (NBTCompound) nbtReader.read(NBTType.TAG_Compound); + BiomeParticle.Option option = () -> particleNBT; + BiomeParticle particle = new BiomeParticle(probability, option); + effectsBuilder.biomeParticle(particle); + } + } + if (effectsJson.has("ambient_sound")) { + effectsBuilder.ambientSound(NamespaceID.from(effectsJson.get("ambient_sound").getAsString())); + } + if (effectsJson.has("mood_sound")) { + JsonObject moodSoundJson = effectsJson.getAsJsonObject("mood_sound"); + NamespaceID sound = NamespaceID.from(moodSoundJson.get("sound").getAsString()); + int tickDelay = moodSoundJson.get("tick_delay").getAsInt(); + int blockSearchExtent = moodSoundJson.get("block_search_extent").getAsInt(); + double offset = moodSoundJson.get("offset").getAsDouble(); + BiomeEffects.MoodSound moodSound = new BiomeEffects.MoodSound(sound, tickDelay, blockSearchExtent, offset); + effectsBuilder.moodSound(moodSound); + } + if (effectsJson.has("additions_sound")) { + JsonObject additionsSoundJson = effectsJson.getAsJsonObject("additions_sound"); + NamespaceID sound = NamespaceID.from(additionsSoundJson.get("sound").getAsString()); + double tickChance = additionsSoundJson.get("tick_chance").getAsDouble(); + BiomeEffects.AdditionsSound additionsSound = new BiomeEffects.AdditionsSound(sound, tickChance); + effectsBuilder.additionsSound(additionsSound); + } + if (effectsJson.has("music")) { + JsonObject musicJson = effectsJson.getAsJsonObject("music"); + NamespaceID sound = NamespaceID.from(musicJson.get("sound").getAsString()); + int minDelay = musicJson.get("min_delay").getAsInt(); + int maxDelay = musicJson.get("max_delay").getAsInt(); + boolean replaceCurrentMusic = musicJson.get("replace_current_music").getAsBoolean(); + BiomeEffects.Music music = new BiomeEffects.Music(sound, minDelay, maxDelay, replaceCurrentMusic); + effectsBuilder.music(music); + } + + biomeBuilder.effects(effectsBuilder.build()); + return biomeBuilder.build(); + } + +} diff --git a/core/src/main/java/org/phantazm/core/equipment/EquipmentHandler.java b/core/src/main/java/org/phantazm/core/equipment/EquipmentHandler.java index 104fd19cf..544d01e83 100644 --- a/core/src/main/java/org/phantazm/core/equipment/EquipmentHandler.java +++ b/core/src/main/java/org/phantazm/core/equipment/EquipmentHandler.java @@ -3,11 +3,14 @@ import com.github.steanky.toolkit.collection.Wrapper; import it.unimi.dsi.fastutil.ints.IntSet; import net.kyori.adventure.key.Key; +import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.phantazm.core.inventory.*; import java.util.*; +import java.util.function.Supplier; public class EquipmentHandler { private final InventoryAccessRegistry accessRegistry; @@ -103,4 +106,91 @@ public void refreshGroup(@NotNull Key groupKey) { } }); } + + public @NotNull Result addOrReplaceEquipment(Key groupKey, Key equipmentKey, boolean allowReplace, int specificSlot, + boolean allowDuplicate, @NotNull Supplier> equipmentSupplier, + @Nullable Player player) { + if (!allowDuplicate && hasEquipment(groupKey, equipmentKey)) { + return Result.DUPLICATE; + } + + boolean addEquipment; + if (!(addEquipment = canAddEquipment(groupKey)) && !allowReplace) { + return Result.FAILED; + } + + InventoryAccessRegistry accessRegistry = accessRegistry(); + Optional inventoryAccessOptional = accessRegistry.getCurrentAccess(); + if (inventoryAccessOptional.isEmpty()) { + return Result.FAILED; + } + + InventoryAccess inventoryAccess = inventoryAccessOptional.get(); + if (addEquipment) { + Optional equipmentOptional = equipmentSupplier.get(); + if (equipmentOptional.isEmpty()) { + return Result.FAILED; + } + + Equipment equipment = equipmentOptional.get(); + if (specificSlot < 0) { + addEquipment(equipment, groupKey); + return Result.ADDED; + } + + InventoryObjectGroup group = inventoryAccess.groups().get(groupKey); + if (invalidGroupSlot(group, specificSlot)) { + return Result.FAILED; + } + + InventoryProfile profile = group.getProfile(); + if (!profile.hasInventoryObject(specificSlot)) { + accessRegistry.replaceObject(specificSlot, equipment); + return Result.ADDED; + } + + InventoryObject currentObject = profile.getInventoryObject(specificSlot); + if (allowReplace || group.defaultObject() == currentObject) { + InventoryObject old = accessRegistry.replaceObject(specificSlot, equipment); + return old == null || old == group.defaultObject() ? Result.ADDED : Result.REPLACED; + } + + return Result.FAILED; + } + + if (player == null) { + return Result.FAILED; + } + + int targetSlot = specificSlot < 0 ? player.getHeldSlot() : specificSlot; + + InventoryObjectGroup group = inventoryAccess.groups().get(groupKey); + if (invalidGroupSlot(group, targetSlot)) { + return Result.FAILED; + } + + Optional equipmentOptional = equipmentSupplier.get(); + if (equipmentOptional.isEmpty()) { + return Result.FAILED; + } + + Equipment equipment = equipmentOptional.get(); + InventoryObject old = accessRegistry.replaceObject(targetSlot, equipment); + return old == null || old == group.defaultObject() ? Result.ADDED : Result.REPLACED; + } + + private boolean invalidGroupSlot(InventoryObjectGroup group, int slot) { + return group == null || !group.getSlots().contains(slot); + } + + public enum Result { + ADDED, + REPLACED, + DUPLICATE, + FAILED; + + public boolean success() { + return this == ADDED || this == REPLACED; + } + } } diff --git a/core/src/main/java/org/phantazm/core/equipment/LinearUpgradePath.java b/core/src/main/java/org/phantazm/core/equipment/LinearUpgradePath.java index 9801881a7..98ec44bed 100644 --- a/core/src/main/java/org/phantazm/core/equipment/LinearUpgradePath.java +++ b/core/src/main/java/org/phantazm/core/equipment/LinearUpgradePath.java @@ -1,5 +1,6 @@ package org.phantazm.core.equipment; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -12,6 +13,7 @@ import java.util.Optional; @Model("zombies.map.shop.upgrade_path.linear") +@Cache public class LinearUpgradePath implements UpgradePath { private final Data data; diff --git a/core/src/main/java/org/phantazm/core/event/PlayerJoinLobbyEvent.java b/core/src/main/java/org/phantazm/core/event/PlayerJoinLobbyEvent.java new file mode 100644 index 000000000..7433e6afb --- /dev/null +++ b/core/src/main/java/org/phantazm/core/event/PlayerJoinLobbyEvent.java @@ -0,0 +1,18 @@ +package org.phantazm.core.event; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.PlayerEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public record PlayerJoinLobbyEvent(Player player) implements PlayerEvent { + public PlayerJoinLobbyEvent(@NotNull Player player) { + this.player = Objects.requireNonNull(player, "player"); + } + + @Override + public @NotNull Player getPlayer() { + return player; + } +} diff --git a/core/src/main/java/org/phantazm/core/game/scene/InstanceScene.java b/core/src/main/java/org/phantazm/core/game/scene/InstanceScene.java index ad27c3618..199372d58 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/InstanceScene.java +++ b/core/src/main/java/org/phantazm/core/game/scene/InstanceScene.java @@ -1,12 +1,17 @@ package org.phantazm.core.game.scene; import com.github.steanky.toolkit.collection.Wrapper; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.player.PlayerView; import java.util.*; +import java.util.concurrent.locks.StampedLock; /** * Basic Scene which corresponds to a single {@link Instance} and {@link SceneFallback} to route players. @@ -17,13 +22,21 @@ public abstract class InstanceScene implement private final UUID uuid; protected final Instance instance; protected final SceneFallback fallback; + protected final Point spawnPoint; - private boolean shutdown = false; + private final Set ghosts; + private final StampedLock ghostLock; - public InstanceScene(@NotNull UUID uuid, @NotNull Instance instance, @NotNull SceneFallback fallback) { + protected volatile boolean shutdown = false; + + public InstanceScene(@NotNull UUID uuid, @NotNull Instance instance, @NotNull SceneFallback fallback, + @NotNull Point spawnPoint) { this.uuid = Objects.requireNonNull(uuid, "uuid"); this.instance = Objects.requireNonNull(instance, "instance"); this.fallback = Objects.requireNonNull(fallback, "fallback"); + this.spawnPoint = Objects.requireNonNull(spawnPoint, "spawnPoint"); + this.ghosts = Collections.newSetFromMap(new WeakHashMap<>()); + this.ghostLock = new StampedLock(); } @Override @@ -58,10 +71,88 @@ public boolean isShutdown() { @Override public void shutdown() { shutdown = true; + MinecraftServer.getInstanceManager().forceUnregisterInstance(instance); } @Override public @NotNull SceneFallback getFallback() { return fallback; } + + @Override + public boolean acceptGhost(@NotNull PlayerView playerView) { + Optional playerOptional = playerView.getPlayer(); + if (playerOptional.isEmpty()) { + return false; + } + + Player player = playerOptional.get(); + Instance oldInstance; + if ((oldInstance = player.getInstance()) == instance) { + return false; + } + + long writeStamp = ghostLock.writeLock(); + try { + ghosts.add(player); + ghosts.removeIf(this::invalidGhost); + } + finally { + ghostLock.unlockWrite(writeStamp); + } + + player.setInstanceAddCallback( + () -> Utils.handleInstanceTransfer(oldInstance, instance, player, newInstancePlayer -> true, + this::hasGhost)); + player.setGameMode(GameMode.SPECTATOR); + player.setInstance(instance, spawnPoint); + return true; + } + + @Override + public boolean hasGhost(@NotNull Player player) { + long optimisticRead = ghostLock.tryOptimisticRead(); + if (ghostLock.validate(optimisticRead)) { + boolean result = ghosts.contains(player); + if (ghostLock.validate(optimisticRead)) { + if (result && invalidGhost(player)) { + cleanGhosts(); + return false; + } + + return result; + } + } + + long readStamp = ghostLock.readLock(); + try { + if (!ghosts.contains(player)) { + return false; + } + } + finally { + ghostLock.unlockRead(readStamp); + } + + if (invalidGhost(player)) { + cleanGhosts(); + return false; + } + + return true; + } + + private void cleanGhosts() { + long writeStamp = ghostLock.writeLock(); + try { + ghosts.removeIf(this::invalidGhost); + } + finally { + ghostLock.unlockWrite(writeStamp); + } + } + + private boolean invalidGhost(Player player) { + return !player.isOnline() || player.getInstance() != instance; + } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/Scene.java b/core/src/main/java/org/phantazm/core/game/scene/Scene.java index 352c305a0..f15cd3804 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/Scene.java +++ b/core/src/main/java/org/phantazm/core/game/scene/Scene.java @@ -1,5 +1,6 @@ package org.phantazm.core.game.scene; +import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.commons.Tickable; @@ -79,4 +80,7 @@ public interface Scene extends Tickable { boolean isQuittable(); + boolean acceptGhost(@NotNull PlayerView playerView); + + boolean hasGhost(@NotNull Player player); } diff --git a/core/src/main/java/org/phantazm/core/game/scene/SceneProvider.java b/core/src/main/java/org/phantazm/core/game/scene/SceneProvider.java index c9be2f678..12ba8f1de 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/SceneProvider.java +++ b/core/src/main/java/org/phantazm/core/game/scene/SceneProvider.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * A provider to create new {@link Scene}s. @@ -20,7 +21,7 @@ public interface SceneProvider, TRequest extends * @param request The request used to provide an appropriate scene * @return An {@link Optional} of a {@link Scene}. The scene may be newly created or from the provider's store. */ - @NotNull Optional provideScene(@NotNull TRequest request); + @NotNull CompletableFuture> provideScene(@NotNull TRequest request); /** * Gets the {@link Scene}s currently stored by the {@link Scene} provider. diff --git a/core/src/main/java/org/phantazm/core/game/scene/SceneProviderAbstract.java b/core/src/main/java/org/phantazm/core/game/scene/SceneProviderAbstract.java index c193d2ba7..f2a19a369 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/SceneProviderAbstract.java +++ b/core/src/main/java/org/phantazm/core/game/scene/SceneProviderAbstract.java @@ -7,14 +7,20 @@ import org.phantazm.core.game.scene.event.SceneShutdownEvent; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.StampedLock; /** * An abstract base for {@link SceneProvider}s. */ public abstract class SceneProviderAbstract, TRequest extends SceneJoinRequest> implements SceneProvider { - private final Collection scenes = new ArrayList<>(); + private final List scenes = new CopyOnWriteArrayList<>(); private final Collection unmodifiableScenes = Collections.unmodifiableCollection(scenes); + private final StampedLock lock = new StampedLock(); + private final Executor executor; private final int maximumScenes; /** @@ -22,22 +28,46 @@ public abstract class SceneProviderAbstract, TReq * * @param maximumScenes The maximum number of {@link Scene}s in the provider. */ - public SceneProviderAbstract(int maximumScenes) { + public SceneProviderAbstract(@NotNull Executor executor, int maximumScenes) { + this.executor = Objects.requireNonNull(executor, "executor"); this.maximumScenes = maximumScenes; } @Override - public @NotNull Optional provideScene(@NotNull TRequest request) { - return Optional.ofNullable(chooseScene(request).orElseGet(() -> { - if (scenes.size() >= maximumScenes) { - return null; + public @NotNull CompletableFuture> provideScene(@NotNull TRequest request) { + return CompletableFuture.supplyAsync(() -> { + long optimisticReadStamp = lock.tryOptimisticRead(); + if (lock.validate(optimisticReadStamp)) { + Optional sceneOptional = chooseScene(request); + + if (lock.validate(optimisticReadStamp) && sceneOptional.isPresent()) { + return sceneOptional; + } } - TScene newScene = createScene(request); - scenes.add(newScene); + long writeStamp = lock.writeLock(); + try { + Optional sceneOptional = chooseScene(request); + if (sceneOptional.isPresent()) { + return sceneOptional; + } - return newScene; - })); + if (scenes.size() >= maximumScenes) { + return Optional.empty(); + } + + TScene scene = createScene(request).join(); + if (scene != null) { + scenes.add(scene); + return Optional.of(scene); + } + + return Optional.empty(); + } + finally { + lock.unlockWrite(writeStamp); + } + }, executor); } @Override @@ -56,14 +86,12 @@ public void forceShutdown() { @Override public void tick(long time) { - Iterator iterator = scenes.iterator(); - - while (iterator.hasNext()) { - TScene scene = iterator.next(); + for (int i = scenes.size() - 1; i >= 0; --i) { + TScene scene = scenes.get(i); if (scene.isShutdown()) { cleanupScene(scene); - iterator.remove(); + scenes.remove(i); ServerProcess process = MinecraftServer.process(); if (process != null) { @@ -90,7 +118,7 @@ public void tick(long time) { * @param request The join request which triggered the creation of the {@link Scene} * @return The new {@link Scene} */ - protected abstract @NotNull TScene createScene(@NotNull TRequest request); + protected abstract @NotNull CompletableFuture createScene(@NotNull TRequest request); protected abstract void cleanupScene(@NotNull TScene scene); diff --git a/core/src/main/java/org/phantazm/core/game/scene/SceneRouter.java b/core/src/main/java/org/phantazm/core/game/scene/SceneRouter.java index 88c32ea42..5b7fc4c33 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/SceneRouter.java +++ b/core/src/main/java/org/phantazm/core/game/scene/SceneRouter.java @@ -1,15 +1,17 @@ package org.phantazm.core.game.scene; +import net.kyori.adventure.key.Keyed; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Tickable; import java.util.Collection; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; -public interface SceneRouter, TRequest extends SceneJoinRequest> extends Tickable { +public interface SceneRouter, TRequest extends SceneJoinRequest> extends Tickable, Keyed { - @NotNull RouteResult findScene(@NotNull TRequest joinRequest); + @NotNull CompletableFuture> findScene(@NotNull TRequest joinRequest); @NotNull Collection getScenes(); diff --git a/core/src/main/java/org/phantazm/core/game/scene/SceneTransferHelper.java b/core/src/main/java/org/phantazm/core/game/scene/SceneTransferHelper.java index f68f815ac..f73a04b8f 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/SceneTransferHelper.java +++ b/core/src/main/java/org/phantazm/core/game/scene/SceneTransferHelper.java @@ -1,5 +1,6 @@ package org.phantazm.core.game.scene; +import it.unimi.dsi.fastutil.booleans.BooleanObjectPair; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.NotNull; @@ -17,6 +18,35 @@ public SceneTransferHelper(@NotNull RouterStore routerStore) { public void transfer(@NotNull Scene to, @NotNull TJoinRequest joinRequest, @NotNull Collection leavers, @NotNull PlayerView leader) { + BooleanObjectPair> result = leaveOldScenes(leavers, to); + + if (result.firstBoolean()) { + leader.getPlayer().ifPresent(leaderPlayer -> { + leaderPlayer.sendMessage( + Component.text("Failed to join because not all players could leave their " + "previous scenes.", + NamedTextColor.RED)); + }); + return; + } + + for (Runnable leaveExecutor : result.second()) { + leaveExecutor.run(); + } + + try (TransferResult joinResult = to.join(joinRequest)) { + if (joinResult.executor().isPresent()) { + joinResult.executor().get().run(); + } + else if (joinResult.message().isPresent()) { + leader.getPlayer().ifPresent(leaderPlayer -> { + leaderPlayer.sendMessage(joinResult.message().get()); + }); + } + } + } + + private BooleanObjectPair> leaveOldScenes(Collection leavers, + Scene to) { boolean anyFailures = false; Collection leaveExecutors = new ArrayList<>(leavers.size()); for (PlayerView leaver : leavers) { @@ -30,40 +60,37 @@ public void transfer(@NotNull Scene { - leaver.getPlayer().ifPresent(player -> player.sendMessage(message)); - }); + try (TransferResult leaveResult = oldScene.leave(Collections.singleton(leaver.getUUID()))) { + if (leaveResult.executor().isPresent()) { + leaveExecutors.add(leaveResult.executor().get()); + } + else { + anyFailures = true; + leaveResult.message().ifPresent(message -> { + leaver.getPlayer().ifPresent(player -> player.sendMessage(message)); + }); + } } } - if (anyFailures) { - leader.getPlayer().ifPresent(leaderPlayer -> { - leaderPlayer.sendMessage( + return BooleanObjectPair.of(anyFailures, leaveExecutors); + } + + public void ghost(@NotNull Scene target, @NotNull PlayerView ghoster) { + BooleanObjectPair> result = leaveOldScenes(List.of(ghoster), null); + if (result.firstBoolean()) { + ghoster.getPlayer().ifPresent(ghosterPlayer -> { + ghosterPlayer.sendMessage( Component.text("Failed to join because not all players could leave their " + "previous scenes.", NamedTextColor.RED)); }); return; } - for (Runnable leaveExecutor : leaveExecutors) { - leaveExecutor.run(); - } - - TransferResult joinResult = to.join(joinRequest); - if (joinResult.executor().isPresent()) { - joinResult.executor().get().run(); - } - else if (joinResult.message().isPresent()) { - leader.getPlayer().ifPresent(leaderPlayer -> { - leaderPlayer.sendMessage(joinResult.message().get()); - }); + if (target.acceptGhost(ghoster)) { + for (Runnable leaveExecutor : result.second()) { + leaveExecutor.run(); + } } } - } diff --git a/core/src/main/java/org/phantazm/core/game/scene/TransferResult.java b/core/src/main/java/org/phantazm/core/game/scene/TransferResult.java index 52d276736..1de50e17f 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/TransferResult.java +++ b/core/src/main/java/org/phantazm/core/game/scene/TransferResult.java @@ -4,13 +4,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.Closeable; import java.util.Objects; import java.util.Optional; /** * Represents the result of transferring a player to or from a {@link Scene}. */ -public record TransferResult(@NotNull Optional executor, @NotNull Optional message) { +public record TransferResult(@NotNull Optional executor, + @NotNull Runnable end, + @NotNull Optional message) implements Closeable { /** * Creates a transfer result. @@ -23,11 +26,25 @@ public record TransferResult(@NotNull Optional executor, @NotNull Opti } public static @NotNull TransferResult success(@NotNull Runnable executor) { - return new TransferResult(Optional.of(executor), Optional.empty()); + return new TransferResult(Optional.of(executor), () -> { + }, Optional.empty()); + } + + public static @NotNull TransferResult success(@NotNull Runnable executor, @NotNull Runnable end) { + return new TransferResult(Optional.of(executor), end, Optional.empty()); } public static @NotNull TransferResult failure(@Nullable Component message) { - return new TransferResult(Optional.empty(), Optional.ofNullable(message)); + return new TransferResult(Optional.empty(), () -> { + }, Optional.ofNullable(message)); } + public static @NotNull TransferResult failure(@Nullable Component message, @NotNull Runnable end) { + return new TransferResult(Optional.empty(), end, Optional.ofNullable(message)); + } + + @Override + public void close() { + end.run(); + } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/Utils.java b/core/src/main/java/org/phantazm/core/game/scene/Utils.java index c225b4ea3..682e5ad4c 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/Utils.java +++ b/core/src/main/java/org/phantazm/core/game/scene/Utils.java @@ -1,24 +1,31 @@ package org.phantazm.core.game.scene; -import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import net.minestom.server.network.packet.server.ServerPacket; -import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; public class Utils { /** - * Handles player transfer between instances, sending list packets. + * Handles player transfer between instances, sending list packets. Will optionally add this player to the tab + * list of other players in the instance. This method must be called before spawn packets are sent to players + * in the target instance. * - * @param oldInstance the old instance; is {@code null} if the player is logging in for the first time - * @param player the player + * @param oldInstance the old instance; is {@code null} if the player is logging in for the first time + * @param newInstance the new instance, if this is the same object as oldInstance, this method will do nothing + * @param player the player + * @param transferPlayerCanSeeInstancePlayer a predicate to test whether a player in {@code newInstance} should be + * visible by the transferring player {@code player} + * @param instancePlayerCanSeeTransferPlayer a predicate to test whether a player in {@code newInstance} should receive a tablist packet */ - public static void handleInstanceTransfer(@NotNull Instance oldInstance, @NotNull Player player) { - Instance newInstance = Objects.requireNonNull(player.getInstance(), "player instance"); + public static void handleInstanceTransfer(@Nullable Instance oldInstance, @NotNull Instance newInstance, + @NotNull Player player, @NotNull Predicate transferPlayerCanSeeInstancePlayer, + @NotNull Predicate instancePlayerCanSeeTransferPlayer) { if (newInstance == oldInstance) { return; } @@ -26,23 +33,45 @@ public static void handleInstanceTransfer(@NotNull Instance oldInstance, @NotNul ServerPacket playerRemove = player.getRemovePlayerToList(); ServerPacket playerAdd = player.getAddPlayerToList(); - for (Player oldPlayer : oldInstance.getEntityTracker().entities(EntityTracker.Target.PLAYERS)) { - oldPlayer.sendPacket(playerRemove); - player.sendPacket(oldPlayer.getRemovePlayerToList()); + if (oldInstance != null) { + for (Player oldPlayer : oldInstance.getEntityTracker().entities(EntityTracker.Target.PLAYERS)) { + if (oldPlayer == player) { + continue; + } + + oldPlayer.sendPacket(playerRemove); + player.sendPacket(oldPlayer.getRemovePlayerToList()); + } } - for (Player newInstancePlayer : newInstance.getEntityTracker().entities(EntityTracker.Target.PLAYERS)) { + Set instancePlayers = newInstance.getEntityTracker().entities(EntityTracker.Target.PLAYERS); + for (Player newInstancePlayer : instancePlayers) { if (newInstancePlayer == player) { continue; } - player.sendPacket(newInstancePlayer.getAddPlayerToList()); - newInstancePlayer.sendPacket(playerAdd); + if (transferPlayerCanSeeInstancePlayer.test(newInstancePlayer)) { + player.sendPacket(newInstancePlayer.getAddPlayerToList()); + } - MinecraftServer.getSchedulerManager().buildTask(() -> { - player.updateNewViewer(newInstancePlayer); - newInstancePlayer.updateNewViewer(player); - }).delay(20, TimeUnit.SERVER_TICK).schedule(); + if (instancePlayerCanSeeTransferPlayer.test(newInstancePlayer)) { + newInstancePlayer.sendPacket(playerAdd); + } } } + + /** + * Handles player transfer between instances, sending list packets. Will also notify players in the new instance + * (the player's current instance). + * + * @param oldInstance the old instance; is {@code null} if the player is logging in for the first time + * @param player the player + * @param transferPlayerCanSeeInstancePlayer a predicate to test whether a player in {@code newInstance} should be + * visible by the transferring player {@code player} + */ + public static void handleInstanceTransfer(@Nullable Instance oldInstance, @NotNull Instance newInstance, + @NotNull Player player, @NotNull Predicate transferPlayerCanSeeInstancePlayer) { + handleInstanceTransfer(oldInstance, newInstance, player, transferPlayerCanSeeInstancePlayer, + newInstancePlayer -> true); + } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/command/QuitCommand.java b/core/src/main/java/org/phantazm/core/game/scene/command/QuitCommand.java index aad743c74..4144bc6b5 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/command/QuitCommand.java +++ b/core/src/main/java/org/phantazm/core/game/scene/command/QuitCommand.java @@ -6,20 +6,27 @@ import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; import org.phantazm.core.game.scene.RouterStore; +import org.phantazm.core.game.scene.Scene; import org.phantazm.core.game.scene.TransferResult; +import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.player.PlayerViewProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Objects; +import java.util.Optional; public final class QuitCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(QuitCommand.class); + private QuitCommand() { throw new UnsupportedOperationException(); } public static @NotNull Command quitCommand(@NotNull RouterStore routerStore, - @NotNull PlayerViewProvider viewProvider) { + @NotNull PlayerViewProvider viewProvider, @NotNull SceneFallback defaultFallback) { Objects.requireNonNull(routerStore, "routerStore"); Objects.requireNonNull(viewProvider, "viewProvider"); @@ -37,7 +44,9 @@ private QuitCommand() { return true; }, (sender, context) -> { Player player = (Player)sender; - routerStore.getCurrentScene(player.getUuid()).ifPresent(scene -> { + Optional> sceneOptional = routerStore.getCurrentScene(player.getUuid()); + if (sceneOptional.isPresent()) { + Scene scene = sceneOptional.get(); if (!scene.isQuittable()) { sender.sendMessage(Component.text("You can't quit this scene.", NamedTextColor.RED)); return; @@ -46,12 +55,24 @@ private QuitCommand() { TransferResult result = scene.leave(Collections.singleton(player.getUuid())); if (result.executor().isPresent()) { result.executor().get().run(); - scene.getFallback().fallback(viewProvider.fromPlayer(player)); + scene.getFallback().fallback(viewProvider.fromPlayer(player)) + .whenComplete((fallbackResult, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fallback", throwable); + } + }); } else { result.message().ifPresent(sender::sendMessage); } - }); + } + else { + defaultFallback.fallback(viewProvider.fromPlayer(player)).whenComplete((fallbackResult, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fallback", throwable); + } + }); + } }); return command; diff --git a/core/src/main/java/org/phantazm/core/game/scene/fallback/CompositeFallback.java b/core/src/main/java/org/phantazm/core/game/scene/fallback/CompositeFallback.java index 27ed7db7e..fe08dd861 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/fallback/CompositeFallback.java +++ b/core/src/main/java/org/phantazm/core/game/scene/fallback/CompositeFallback.java @@ -3,7 +3,9 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.player.PlayerView; +import java.util.Iterator; import java.util.Objects; +import java.util.concurrent.CompletableFuture; /** * A {@link SceneFallback} which delegates to multiple sub-{@link SceneFallback}s. @@ -23,14 +25,25 @@ public CompositeFallback(@NotNull Iterable fallbacks) { } @Override - public boolean fallback(@NotNull PlayerView player) { - for (SceneFallback fallback : fallbacks) { - if (fallback.fallback(player)) { - return true; - } + public CompletableFuture fallback(@NotNull PlayerView player) { + Iterator iterator = fallbacks.iterator(); + if (!iterator.hasNext()) { + return CompletableFuture.completedFuture(false); } - return false; + CompletableFuture future = iterator.next().fallback(player); + while (iterator.hasNext()) { + SceneFallback fallback = iterator.next(); + future = future.thenCompose(result -> { + if (result) { + return CompletableFuture.completedFuture(true); + } + + return fallback.fallback(player); + }); + } + + return future; } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/fallback/KickFallback.java b/core/src/main/java/org/phantazm/core/game/scene/fallback/KickFallback.java index 8873fe9b5..8afe8f7d8 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/fallback/KickFallback.java +++ b/core/src/main/java/org/phantazm/core/game/scene/fallback/KickFallback.java @@ -7,6 +7,7 @@ import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * A {@link SceneFallback} which kicks {@link Player}s. @@ -25,14 +26,14 @@ public KickFallback(@NotNull Component kickMessage) { } @Override - public boolean fallback(@NotNull PlayerView playerView) { + public CompletableFuture fallback(@NotNull PlayerView playerView) { Optional playerOptional = playerView.getPlayer(); if (playerOptional.isPresent()) { playerOptional.get().kick(kickMessage); - return true; + return CompletableFuture.completedFuture(true); } - return false; + return CompletableFuture.completedFuture(false); } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/fallback/SceneFallback.java b/core/src/main/java/org/phantazm/core/game/scene/fallback/SceneFallback.java index 066283e07..a1060105c 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/fallback/SceneFallback.java +++ b/core/src/main/java/org/phantazm/core/game/scene/fallback/SceneFallback.java @@ -3,6 +3,8 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.player.PlayerView; +import java.util.concurrent.CompletableFuture; + /** * A fallback for scenes to route players to when necessary. */ @@ -15,6 +17,6 @@ public interface SceneFallback { * @param player The {@link PlayerView} to handle * @return Whether the fallback was successful */ - boolean fallback(@NotNull PlayerView player); + CompletableFuture fallback(@NotNull PlayerView player); } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyJoinRequest.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyJoinRequest.java index 52a8cf99d..a31f50232 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyJoinRequest.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyJoinRequest.java @@ -1,10 +1,7 @@ package org.phantazm.core.game.scene.lobby; -import it.unimi.dsi.fastutil.Pair; import net.minestom.server.entity.GameMode; -import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; -import net.minestom.server.network.ConnectionManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.core.config.InstanceConfig; @@ -22,10 +19,6 @@ */ public class BasicLobbyJoinRequest implements LobbyJoinRequest { - private static final CompletableFuture[] EMPTY_COMPLETABLE_FUTURE_ARRAY = new CompletableFuture[0]; - - private final ConnectionManager connectionManager; - private final Collection players; /** @@ -33,9 +26,7 @@ public class BasicLobbyJoinRequest implements LobbyJoinRequest { * * @param players The players in the request */ - public BasicLobbyJoinRequest(@NotNull ConnectionManager connectionManager, - @NotNull Collection players) { - this.connectionManager = Objects.requireNonNull(connectionManager, "connectionManager"); + public BasicLobbyJoinRequest(@NotNull Collection players) { this.players = Objects.requireNonNull(players, "players"); } @@ -45,33 +36,25 @@ public BasicLobbyJoinRequest(@NotNull ConnectionManager connectionManager, } @Override - public void handleJoin(@NotNull Instance instance, @NotNull InstanceConfig instanceConfig) { - List> teleportedPlayers = new ArrayList<>(players.size()); + public void handleJoin(@NotNull Lobby lobby, @NotNull Instance instance, @NotNull InstanceConfig instanceConfig) { List> futures = new ArrayList<>(players.size()); for (PlayerView view : players) { view.getPlayer().ifPresent(player -> { player.setGameMode(GameMode.ADVENTURE); - teleportedPlayers.add(Pair.of(player, player.getInstance())); if (player.getInstance() == instance) { futures.add(player.teleport(instanceConfig.spawnPoint())); } else { + Instance oldInstance = player.getInstance(); + player.setInstanceAddCallback(() -> Utils.handleInstanceTransfer(oldInstance, instance, player, + newInstancePlayer -> !lobby.hasGhost(newInstancePlayer))); futures.add(player.setInstance(instance, instanceConfig.spawnPoint())); } }); } - CompletableFuture.allOf(futures.toArray(EMPTY_COMPLETABLE_FUTURE_ARRAY)).thenRun(() -> { - for (int i = 0; i < futures.size(); ++i) { - Pair pair = teleportedPlayers.get(i); - - Player teleportedPlayer = pair.first(); - Instance oldInstance = pair.second(); - - Utils.handleInstanceTransfer(oldInstance, teleportedPlayer); - } - }).join(); + CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join(); } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyProvider.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyProvider.java index c5071e0a5..4d0ed96b2 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyProvider.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/BasicLobbyProvider.java @@ -5,6 +5,10 @@ import com.github.steanky.element.core.context.ElementContext; import com.github.steanky.ethylene.core.ConfigElement; import com.github.steanky.ethylene.core.collection.ConfigList; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.minestom.server.entity.Player; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventNode; import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.event.item.ItemDropEvent; @@ -13,10 +17,14 @@ import net.minestom.server.event.player.*; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.OpenBookPacket; import org.jetbrains.annotations.NotNull; import org.phantazm.core.ElementUtils; import org.phantazm.core.config.InstanceConfig; import org.phantazm.core.game.scene.SceneProviderAbstract; +import org.phantazm.core.game.scene.TransferResult; import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.instance.InstanceLoader; import org.phantazm.core.npc.NPC; @@ -25,6 +33,8 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -39,7 +49,11 @@ public class BasicLobbyProvider extends LobbyProviderAbstract { private final SceneFallback fallback; private final InstanceConfig instanceConfig; private final List npcContexts; + private final Collection defaultItems; + private final MiniMessage miniMessage; + private final String lobbyJoinFormat; private final boolean quittable; + private final EventNode rootNode; /** * Creates a basic implementation of a {@link SceneProviderAbstract}. @@ -52,10 +66,13 @@ public class BasicLobbyProvider extends LobbyProviderAbstract { * @param fallback A {@link SceneFallback} for the created {@link Lobby}s * @param instanceConfig The {@link InstanceConfig} for the {@link Lobby}s */ - public BasicLobbyProvider(int maximumLobbies, int newLobbyThreshold, @NotNull InstanceLoader instanceLoader, - @NotNull List lobbyPaths, @NotNull SceneFallback fallback, @NotNull InstanceConfig instanceConfig, - @NotNull ContextManager contextManager, @NotNull ConfigList npcConfigs, boolean quittable) { - super(maximumLobbies, newLobbyThreshold); + public BasicLobbyProvider(@NotNull Executor executor, int maximumLobbies, int newLobbyThreshold, + @NotNull InstanceLoader instanceLoader, @NotNull List lobbyPaths, @NotNull SceneFallback fallback, + @NotNull InstanceConfig instanceConfig, @NotNull ContextManager contextManager, + @NotNull ConfigList npcConfigs, @NotNull Collection defaultItems, + @NotNull MiniMessage miniMessage, @NotNull String lobbyJoinFormat, boolean quittable, + @NotNull EventNode rootNode) { + super(executor, maximumLobbies, newLobbyThreshold); this.instanceLoader = Objects.requireNonNull(instanceLoader, "instanceLoader"); this.lobbyPaths = List.copyOf(Objects.requireNonNull(lobbyPaths, "lobbyPaths")); @@ -71,49 +88,67 @@ public BasicLobbyProvider(int maximumLobbies, int newLobbyThreshold, @NotNull In } this.npcContexts = List.copyOf(npcContexts); + this.defaultItems = Objects.requireNonNull(defaultItems, "defaultItems"); + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + this.lobbyJoinFormat = Objects.requireNonNull(lobbyJoinFormat, "lobbyJoinFormat"); this.quittable = quittable; + this.rootNode = rootNode; } @Override - protected @NotNull Lobby createScene(@NotNull LobbyJoinRequest request) { - Instance instance = instanceLoader.loadInstance(lobbyPaths); - instance.setTime(instanceConfig.time()); - instance.setTimeRate(instanceConfig.timeRate()); + protected @NotNull CompletableFuture createScene(@NotNull LobbyJoinRequest request) { + return instanceLoader.loadInstance(lobbyPaths).thenApply(instance -> { + instance.setTime(instanceConfig.time()); + instance.setTimeRate(instanceConfig.timeRate()); - EventNode eventNode = instance.eventNode(); - eventNode.addListener(PlayerSwapItemEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(ItemDropEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(InventoryPreClickEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PlayerPreEatEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PickupItemEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PickupExperienceEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PrePlayerStartDiggingEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true)); - eventNode.addListener(PlayerBlockInteractEvent.class, event -> { - event.setCancelled(true); - event.setBlockingItemUse(true); - }); - eventNode.addListener(PlayerBlockBreakEvent.class, event -> event.setCancelled(true)); + EventNode instanceNode = + EventNode.type("instance_npc_node_" + instance.getUniqueId(), EventFilter.INSTANCE, + (e, v) -> v == instance); - List npcs = new ArrayList<>(npcContexts.size()); - for (ElementContext context : npcContexts) { - NPC npc = context.provide(HANDLER, () -> null); - if (npc != null) { - npcs.add(npc); - } - } + instanceNode.addListener(PlayerSwapItemEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(ItemDropEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(InventoryPreClickEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PlayerPreEatEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PickupItemEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PickupExperienceEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PrePlayerStartDiggingEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PlayerBlockInteractEvent.class, event -> { + event.setCancelled(true); + event.setBlockingItemUse(true); + }); + instanceNode.addListener(PlayerBlockBreakEvent.class, event -> event.setCancelled(true)); + instanceNode.addListener(PlayerUseItemEvent.class, event -> { + if (event.getPlayer().getItemInMainHand().material().equals(Material.WRITTEN_BOOK)) { + event.getPlayer().sendPacket(new OpenBookPacket(Player.Hand.MAIN)); + } + }); - Lobby lobby = new Lobby(UUID.randomUUID(), instance, instanceConfig, fallback, - new NPCHandler(List.copyOf(npcs), instance), quittable); - eventNode.addListener(PlayerDisconnectEvent.class, - event -> lobby.leave(Collections.singleton(event.getPlayer().getUuid())).executor() - .ifPresent(Runnable::run)); + List npcs = new ArrayList<>(npcContexts.size()); + for (ElementContext context : npcContexts) { + NPC npc = context.provide(HANDLER, () -> null); + if (npc != null) { + npcs.add(npc); + } + } - return lobby; + Lobby lobby = new Lobby(UUID.randomUUID(), instance, instanceConfig, fallback, + new NPCHandler(List.copyOf(npcs), instance, instanceNode), defaultItems, miniMessage, + lobbyJoinFormat, quittable); + instanceNode.addListener(PlayerDisconnectEvent.class, event -> { + try (TransferResult result = lobby.leave(Collections.singleton(event.getPlayer().getUuid()))) { + result.executor().ifPresent(Runnable::run); + } + }); + rootNode.addChild(instanceNode); + return lobby; + }); } @Override protected void cleanupScene(@NotNull Lobby scene) { - scene.cleanup(); + NPCHandler handler = scene.handler(); + handler.end(); + rootNode.removeChild(handler.instanceNode()); } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/Lobby.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/Lobby.java index 8459c419b..24b9aa962 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/Lobby.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/Lobby.java @@ -2,16 +2,24 @@ import it.unimi.dsi.fastutil.Pair; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.core.config.InstanceConfig; +import org.phantazm.core.event.PlayerJoinLobbyEvent; import org.phantazm.core.game.scene.InstanceScene; import org.phantazm.core.game.scene.TransferResult; import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.npc.NPCHandler; import org.phantazm.core.player.PlayerView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; @@ -19,9 +27,13 @@ * Represents a lobby. Most basic scene which contains {@link Player}s. */ public class Lobby extends InstanceScene { + private static final Logger LOGGER = LoggerFactory.getLogger(Lobby.class); private final InstanceConfig instanceConfig; private final Map players; private final NPCHandler npcHandler; + private final Collection defaultItems; + private final MiniMessage miniMessage; + private final String lobbyJoinFormat; private final boolean quittable; private boolean joinable = true; @@ -34,12 +46,17 @@ public class Lobby extends InstanceScene { * @param fallback A fallback for the lobby */ public Lobby(@NotNull UUID uuid, @NotNull Instance instance, @NotNull InstanceConfig instanceConfig, - @NotNull SceneFallback fallback, @NotNull NPCHandler npcHandler, boolean quittable) { - super(uuid, instance, fallback); + @NotNull SceneFallback fallback, @NotNull NPCHandler npcHandler, + @NotNull Collection defaultItems, @NotNull MiniMessage miniMessage, + @NotNull String lobbyJoinFormat, boolean quittable) { + super(uuid, instance, fallback, instanceConfig.spawnPoint()); this.instanceConfig = Objects.requireNonNull(instanceConfig, "instanceConfig"); this.players = new HashMap<>(); this.npcHandler = Objects.requireNonNull(npcHandler, "npcHandler"); this.npcHandler.spawnAll(); + this.defaultItems = Objects.requireNonNull(defaultItems, "defaultItems"); + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + this.lobbyJoinFormat = Objects.requireNonNull(lobbyJoinFormat, "lobbyJoinFormat"); this.quittable = quittable; } @@ -71,7 +88,20 @@ public Lobby(@NotNull UUID uuid, @NotNull Instance instance, @NotNull InstanceCo players.put(player.first().getUUID(), player.first()); } - joinRequest.handleJoin(instance, instanceConfig); + joinRequest.handleJoin(this, instance, instanceConfig); + for (Pair player : joiners) { + player.left().getDisplayName().thenAccept(joiner -> { + TagResolver joinerPlaceholder = Placeholder.component("joiner", joiner); + Component message = miniMessage.deserialize(lobbyJoinFormat, joinerPlaceholder); + instance.sendMessage(message); + }); + + for (ItemStack stack : defaultItems) { + player.right().getInventory().addItemStack(stack); + } + + EventDispatcher.call(new PlayerJoinLobbyEvent(player.right())); + } }); } @@ -119,18 +149,22 @@ public boolean isQuittable() { @Override public void shutdown() { for (PlayerView player : players.values()) { - fallback.fallback(player); + fallback.fallback(player).whenComplete((fallbackResult, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fallback {}", player.getUUID(), throwable); + } + }); } super.shutdown(); } - public void cleanup() { - this.npcHandler.end(); - } - @Override public void tick(long time) { this.npcHandler.tick(time); } + + public @NotNull NPCHandler handler() { + return this.npcHandler; + } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyJoinRequest.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyJoinRequest.java index 827c12a95..8adc58ccf 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyJoinRequest.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyJoinRequest.java @@ -26,7 +26,7 @@ public interface LobbyJoinRequest extends SceneJoinRequest { * @param instance The {@link Instance} the players are joining * @param instanceConfig Configuration for the {@link Instance} */ - void handleJoin(@NotNull Instance instance, @NotNull InstanceConfig instanceConfig); + void handleJoin(@NotNull Lobby lobby, @NotNull Instance instance, @NotNull InstanceConfig instanceConfig); @Override default int getRequestWeight() { diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyProviderAbstract.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyProviderAbstract.java index 127309a43..9dc2ce212 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyProviderAbstract.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyProviderAbstract.java @@ -6,6 +6,7 @@ import org.phantazm.core.player.PlayerView; import java.util.Optional; +import java.util.concurrent.Executor; /** * An abstract {@link Lobby} {@link SceneProvider}. @@ -21,8 +22,8 @@ public abstract class LobbyProviderAbstract extends SceneProviderAbstract chooseScene(@NotNull LobbyJoinRequest request) { Lobby maximumLobby = null; - int maximumWeighting = Integer.MIN_VALUE; + int maximumWeighting = Integer.MAX_VALUE; sceneLoop: for (Lobby lobby : getScenes()) { @@ -42,7 +43,7 @@ public LobbyProviderAbstract(int maximumLobbies, int newLobbyThreshold) { int joinWeight = lobby.getJoinWeight(request); - if (joinWeight > maximumWeighting) { + if (joinWeight < maximumWeighting) { maximumLobby = lobby; maximumWeighting = joinWeight; } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouter.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouter.java index cdb50a422..c5cca5356 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouter.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouter.java @@ -1,18 +1,23 @@ package org.phantazm.core.game.scene.lobby; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Namespaces; import org.phantazm.core.game.scene.RouteResult; import org.phantazm.core.game.scene.Scene; import org.phantazm.core.game.scene.SceneProvider; import org.phantazm.core.game.scene.SceneRouter; import java.util.*; +import java.util.concurrent.CompletableFuture; /** * {@link Scene} router for {@link Lobby}s. */ public class LobbyRouter implements SceneRouter { + public static final Key KEY = Key.key(Namespaces.PHANTAZM, "lobby"); private final Map> lobbyProviders; @@ -58,22 +63,27 @@ public LobbyRouter(@NotNull Map> } @Override - public @NotNull RouteResult findScene(@NotNull LobbyRouteRequest routeRequest) { + public @NotNull CompletableFuture> findScene(@NotNull LobbyRouteRequest routeRequest) { if (isShutdown()) { - return RouteResult.failure(Component.text("The router is shutdown.")); + return CompletableFuture.completedFuture( + RouteResult.failure(Component.text("This game has shut down.", NamedTextColor.RED))); } if (!isJoinable()) { - return RouteResult.failure(Component.text("The router is not joinable.")); + return CompletableFuture.completedFuture( + RouteResult.failure(Component.text("This game is not joinable.", NamedTextColor.RED))); } SceneProvider lobbyProvider = lobbyProviders.get(routeRequest.targetLobbyName()); if (lobbyProvider == null) { - return RouteResult.failure( - Component.text("No lobbies exist under the name " + routeRequest.targetLobbyName() + ".")); + return CompletableFuture.completedFuture(RouteResult.failure( + Component.text("No lobbies exist under the name " + routeRequest.targetLobbyName() + ".", + NamedTextColor.RED))); } - return lobbyProvider.provideScene(routeRequest.joinRequest()).map(RouteResult::success) - .orElseGet(() -> RouteResult.failure(Component.text("No lobbies are joinable."))); + return lobbyProvider.provideScene(routeRequest.joinRequest()).thenApply(sceneOptional -> { + return sceneOptional.map(RouteResult::success).orElseGet( + () -> RouteResult.failure(Component.text("No lobbies are joinable.", NamedTextColor.RED))); + }); } @Override @@ -123,4 +133,8 @@ public void tick(long time) { } } + @Override + public @NotNull Key key() { + return KEY; + } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouterFallback.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouterFallback.java index 3a67dd5e0..91cd56a9f 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouterFallback.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/LobbyRouterFallback.java @@ -1,6 +1,5 @@ package org.phantazm.core.game.scene.lobby; -import net.minestom.server.network.ConnectionManager; import org.jetbrains.annotations.NotNull; import org.phantazm.core.game.scene.TransferResult; import org.phantazm.core.game.scene.fallback.SceneFallback; @@ -8,15 +7,13 @@ import java.util.Collections; import java.util.Objects; -import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * A {@link SceneFallback} which routes to a lobby. */ public class LobbyRouterFallback implements SceneFallback { - private final ConnectionManager connectionManager; - private final LobbyRouter lobbyRouter; private final String lobbyName; @@ -27,30 +24,30 @@ public class LobbyRouterFallback implements SceneFallback { * @param lobby The {@link LobbyRouter} to fallback to * @param lobbyName The name of the {@link Lobby} to fallback to */ - public LobbyRouterFallback(@NotNull ConnectionManager connectionManager, @NotNull LobbyRouter lobby, - @NotNull String lobbyName) { - this.connectionManager = Objects.requireNonNull(connectionManager, "connectionManager"); + public LobbyRouterFallback(@NotNull LobbyRouter lobby, @NotNull String lobbyName) { this.lobbyRouter = Objects.requireNonNull(lobby, "lobbyRouter"); this.lobbyName = Objects.requireNonNull(lobbyName, "lobbyName"); } @Override - public boolean fallback(@NotNull PlayerView player) { - LobbyJoinRequest joinRequest = new BasicLobbyJoinRequest(connectionManager, Collections.singleton(player)); + public CompletableFuture fallback(@NotNull PlayerView player) { + LobbyJoinRequest joinRequest = new BasicLobbyJoinRequest(Collections.singleton(player)); LobbyRouteRequest routeRequest = new LobbyRouteRequest(lobbyName, joinRequest); - Optional lobbyOptional = lobbyRouter.findScene(routeRequest).scene(); - if (lobbyOptional.isEmpty()) { - return false; - } - - Lobby lobby = lobbyOptional.get(); - TransferResult result = lobby.join(joinRequest); - if (result.executor().isEmpty()) { - return false; - } - - result.executor().get().run(); - return true; + return lobbyRouter.findScene(routeRequest).thenApply(routeResult -> { + if (routeResult.scene().isEmpty()) { + return false; + } + + Lobby lobby = routeResult.scene().get(); + try (TransferResult result = lobby.join(joinRequest)) { + if (result.executor().isEmpty()) { + return false; + } + + result.executor().get().run(); + return true; + } + }); } } diff --git a/core/src/main/java/org/phantazm/core/game/scene/lobby/LoginLobbyJoinRequest.java b/core/src/main/java/org/phantazm/core/game/scene/lobby/LoginLobbyJoinRequest.java index f661db0b6..962f84395 100644 --- a/core/src/main/java/org/phantazm/core/game/scene/lobby/LoginLobbyJoinRequest.java +++ b/core/src/main/java/org/phantazm/core/game/scene/lobby/LoginLobbyJoinRequest.java @@ -1,15 +1,12 @@ package org.phantazm.core.game.scene.lobby; import net.minestom.server.entity.GameMode; -import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerLoginEvent; -import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.instance.Instance; -import net.minestom.server.network.ConnectionManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.core.config.InstanceConfig; -import org.phantazm.core.game.scene.Utils; + import org.phantazm.core.player.PlayerView; import org.phantazm.core.player.PlayerViewProvider; @@ -43,7 +40,7 @@ public LoginLobbyJoinRequest(@NotNull PlayerLoginEvent event, @NotNull PlayerVie } @Override - public void handleJoin(@NotNull Instance instance, @NotNull InstanceConfig instanceConfig) { + public void handleJoin(@NotNull Lobby lobby, @NotNull Instance instance, @NotNull InstanceConfig instanceConfig) { event.setSpawningInstance(instance); event.getPlayer().setRespawnPoint(instanceConfig.spawnPoint()); event.getPlayer().setGameMode(GameMode.ADVENTURE); diff --git a/core/src/main/java/org/phantazm/core/gui/Gui.java b/core/src/main/java/org/phantazm/core/gui/Gui.java index 16af241a7..feb91502a 100644 --- a/core/src/main/java/org/phantazm/core/gui/Gui.java +++ b/core/src/main/java/org/phantazm/core/gui/Gui.java @@ -12,6 +12,8 @@ import java.util.*; import java.util.function.BiPredicate; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; /** * Extension of {@link Inventory} designed to ease the creation of graphical user interfaces. May or may not be @@ -121,27 +123,27 @@ public void removeItem(int slot) { @Override public boolean leftClick(@NotNull Player player, int slot) { - return handleClick(player, slot, super::leftClick, GuiItem.ClickType.LEFT_CLICK); + return handleClick(player, slot, () -> super.leftClick(player, slot), GuiItem.ClickType.LEFT_CLICK); } @Override public boolean rightClick(@NotNull Player player, int slot) { - return handleClick(player, slot, super::rightClick, GuiItem.ClickType.RIGHT_CLICK); + return handleClick(player, slot, () -> super.rightClick(player, slot), GuiItem.ClickType.RIGHT_CLICK); } @Override public boolean shiftClick(@NotNull Player player, int slot) { - return handleClick(player, slot, super::shiftClick, GuiItem.ClickType.SHIFT_CLICK); + return handleClick(player, slot, () -> super.shiftClick(player, slot), GuiItem.ClickType.SHIFT_CLICK); } @Override public boolean middleClick(@NotNull Player player, int slot) { - return handleClick(player, slot, super::middleClick, GuiItem.ClickType.MIDDLE_CLICK); + return handleClick(player, slot, () -> super.middleClick(player, slot), GuiItem.ClickType.MIDDLE_CLICK); } @Override - public boolean doubleClick(@NotNull Player player, int slot) { - return handleClick(player, slot, super::doubleClick, GuiItem.ClickType.DOUBLE_CLICK); + public boolean doubleClick(@NotNull Player player, int slot, int button) { + return handleClick(player, slot, () -> super.doubleClick(player, slot, button), GuiItem.ClickType.DOUBLE_CLICK); } @Override @@ -183,8 +185,7 @@ private SlottedItem[] getTickItems() { return tickItems; } - private boolean handleClick(Player player, int slot, BiPredicate superFunction, - GuiItem.ClickType clickType) { + private boolean handleClick(Player player, int slot, BooleanSupplier superFunction, GuiItem.ClickType clickType) { if (slot < getInventoryType().getSize()) { GuiItem item = items.get(slot); if (item != null) { @@ -195,7 +196,7 @@ private boolean handleClick(Player player, int slot, BiPredicate implements Tickable { - private final Object2LongMap latestInviteTimes = new Object2LongArrayMap<>(); + private final Object2LongMap latestInviteTimes = new Object2LongOpenHashMap<>(); private final Queue invitations = new LinkedList<>(); diff --git a/core/src/main/java/org/phantazm/core/guild/party/PartyChatChannel.java b/core/src/main/java/org/phantazm/core/guild/party/PartyChatChannel.java index b0531b9e8..c5640e452 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/PartyChatChannel.java +++ b/core/src/main/java/org/phantazm/core/guild/party/PartyChatChannel.java @@ -5,6 +5,9 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; import org.jetbrains.annotations.NotNull; @@ -14,6 +17,7 @@ import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; @@ -24,8 +28,8 @@ public class PartyChatChannel extends BasicChatChannel { private final Map parties; public PartyChatChannel(@NotNull Map parties, - @NotNull PlayerViewProvider viewProvider) { - super(viewProvider); + @NotNull PlayerViewProvider viewProvider, @NotNull MiniMessage miniMessage, @NotNull String chatFormat) { + super(viewProvider, miniMessage, chatFormat); this.parties = Objects.requireNonNull(parties, "parties"); } @@ -40,9 +44,4 @@ public PartyChatChannel(@NotNull Map parties, return Pair.of(party.getAudience(), null); } - @Override - public @NotNull Component formatMessage(@NotNull PlayerChatEvent chatEvent) { - return Component.textOfChildren(Component.text("Party", NamedTextColor.BLUE), - Component.text(" > ", NamedTextColor.DARK_GRAY), super.formatMessage(chatEvent)); - } } diff --git a/core/src/main/java/org/phantazm/core/guild/party/PartyConfig.java b/core/src/main/java/org/phantazm/core/guild/party/PartyConfig.java index 7457c5d61..405e4d06d 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/PartyConfig.java +++ b/core/src/main/java/org/phantazm/core/guild/party/PartyConfig.java @@ -1,10 +1,21 @@ package org.phantazm.core.guild.party; +import net.minestom.server.MinecraftServer; import org.jetbrains.annotations.NotNull; import org.phantazm.core.guild.party.command.PartyCommandConfig; import org.phantazm.core.guild.party.notification.PartyNotificationConfig; public record PartyConfig(@NotNull PartyNotificationConfig notificationConfig, - @NotNull PartyCommandConfig commandConfig, int creatorRank, int defaultRank, long invitationDuration, int minimumKickRank, int minimumInviteRank, + @NotNull PartyCommandConfig commandConfig, + int creatorRank, + int defaultRank, + long invitationDuration, + int minimumKickRank, + int minimumInviteRank, int minimumJoinRank) { + + public static final PartyConfig DEFAULT = + new PartyConfig(PartyNotificationConfig.DEFAULT, PartyCommandConfig.DEFAULT, 1, 0, + 60L * MinecraftServer.TICK_PER_SECOND, 1, 1, 1); + } diff --git a/core/src/main/java/org/phantazm/core/guild/party/PartyCreator.java b/core/src/main/java/org/phantazm/core/guild/party/PartyCreator.java index d09b2d440..8d05c02b1 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/PartyCreator.java +++ b/core/src/main/java/org/phantazm/core/guild/party/PartyCreator.java @@ -85,8 +85,7 @@ private PartyMember createMember(PlayerView playerView) { public static class Builder { - private PartyNotificationConfig notificationConfig = - new PartyNotificationConfig("", "", "", "", "", "", "", "", "", "", "", ""); + private PartyNotificationConfig notificationConfig = PartyNotificationConfig.DEFAULT; private TickFormatter tickFormatter = new AnalogTickFormatter(new AnalogTickFormatter.Data(false)); diff --git a/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommand.java b/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommand.java index 04a0a9379..1b3b98eab 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommand.java +++ b/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommand.java @@ -18,12 +18,12 @@ private PartyCommand() { public static @NotNull Command partyCommand(@NotNull PartyCommandConfig config, @NotNull MiniMessage miniMessage, @NotNull GuildHolder partyHolder, @NotNull PlayerViewProvider viewProvider, - @NotNull PartyCreator partyCreator, @NotNull Random random) { + @NotNull PartyCreator partyCreator, @NotNull Random random, int creatorRank) { Command command = new Command("party", "p"); command.addSubcommand(PartyCreateCommand.createCommand(config, partyHolder, viewProvider, partyCreator)); command.addSubcommand( PartyJoinCommand.joinCommand(config, miniMessage, partyHolder.uuidToGuild(), viewProvider)); - command.addSubcommand(PartyLeaveCommand.leaveCommand(config, partyHolder.uuidToGuild(), random)); + command.addSubcommand(PartyLeaveCommand.leaveCommand(config, partyHolder.uuidToGuild(), random, creatorRank)); command.addSubcommand(PartyKickCommand.kickCommand(config, miniMessage, partyHolder.uuidToGuild(), viewProvider)); command.addSubcommand( diff --git a/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommandConfig.java b/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommandConfig.java index c86e2e115..12ff86c00 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommandConfig.java +++ b/core/src/main/java/org/phantazm/core/guild/party/command/PartyCommandConfig.java @@ -22,4 +22,11 @@ public record PartyCommandConfig(@NotNull Component notInParty, @NotNull String listFormat, @NotNull String onlineMemberFormat, @NotNull String offlineMemberFormat) { + + public static final PartyCommandConfig DEFAULT = + new PartyCommandConfig(Component.empty(), Component.empty(), Component.empty(), "", + "", Component.empty(), Component.empty(), Component.empty(), Component.empty(), + "", "", Component.empty(), Component.empty(), "", + Component.empty(), "", "", "", ""); + } diff --git a/core/src/main/java/org/phantazm/core/guild/party/command/PartyLeaveCommand.java b/core/src/main/java/org/phantazm/core/guild/party/command/PartyLeaveCommand.java index 9c4ccf5a4..426df4246 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/command/PartyLeaveCommand.java +++ b/core/src/main/java/org/phantazm/core/guild/party/command/PartyLeaveCommand.java @@ -1,11 +1,8 @@ package org.phantazm.core.guild.party.command; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.command.builder.Command; import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.guild.GuildHolder; import org.phantazm.core.guild.party.Party; import org.phantazm.core.guild.party.PartyMember; @@ -18,7 +15,7 @@ private PartyLeaveCommand() { } public static @NotNull Command leaveCommand(@NotNull PartyCommandConfig config, - @NotNull Map partyMap, @NotNull Random random) { + @NotNull Map partyMap, @NotNull Random random, int creatorRank) { Objects.requireNonNull(config, "config"); Objects.requireNonNull(partyMap, "partyMap"); Objects.requireNonNull(random, "random"); @@ -72,6 +69,7 @@ private PartyLeaveCommand() { } party.getOwner().set(newOwner); + newOwner.setRank(creatorRank); party.getNotification().notifyTransfer(oldMember, newOwner); } } diff --git a/core/src/main/java/org/phantazm/core/guild/party/notification/PartyNotificationConfig.java b/core/src/main/java/org/phantazm/core/guild/party/notification/PartyNotificationConfig.java index b2f3bdd0d..a62123cf2 100644 --- a/core/src/main/java/org/phantazm/core/guild/party/notification/PartyNotificationConfig.java +++ b/core/src/main/java/org/phantazm/core/guild/party/notification/PartyNotificationConfig.java @@ -16,4 +16,8 @@ public record PartyNotificationConfig( @NotNull String kickToKickedFormat, @NotNull String transferFormat ) { + + public static final PartyNotificationConfig DEFAULT = new PartyNotificationConfig("", "", "", "", "", "", "", "", + "", "", "", ""); + } diff --git a/core/src/main/java/org/phantazm/core/instance/AnvilFileSystemInstanceLoader.java b/core/src/main/java/org/phantazm/core/instance/AnvilFileSystemInstanceLoader.java index 5b0f3197e..af413f4a2 100644 --- a/core/src/main/java/org/phantazm/core/instance/AnvilFileSystemInstanceLoader.java +++ b/core/src/main/java/org/phantazm/core/instance/AnvilFileSystemInstanceLoader.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import java.nio.file.Path; +import java.util.concurrent.Executor; /** * A {@link FileSystemInstanceLoader} that loads {@link Instance}s using {@link AnvilLoader}s. @@ -21,8 +22,8 @@ public class AnvilFileSystemInstanceLoader extends FileSystemInstanceLoader { * @param chunkSupplier the {@link ChunkSupplier} used to create chunks */ public AnvilFileSystemInstanceLoader(@NotNull InstanceManager instanceManager, @NotNull Path rootPath, - @NotNull ChunkSupplier chunkSupplier) { - super(instanceManager, rootPath, chunkSupplier); + @NotNull ChunkSupplier chunkSupplier, @NotNull Executor executor) { + super(instanceManager, rootPath, chunkSupplier, executor); } @Override diff --git a/core/src/main/java/org/phantazm/core/instance/FileSystemInstanceLoader.java b/core/src/main/java/org/phantazm/core/instance/FileSystemInstanceLoader.java index 8a78b0a86..5f975c395 100644 --- a/core/src/main/java/org/phantazm/core/instance/FileSystemInstanceLoader.java +++ b/core/src/main/java/org/phantazm/core/instance/FileSystemInstanceLoader.java @@ -7,6 +7,7 @@ import net.minestom.server.instance.InstanceManager; import net.minestom.server.utils.chunk.ChunkSupplier; import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; @@ -14,8 +15,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Phaser; +import java.util.UUID; +import java.util.concurrent.*; /** * Implements an {@link InstanceLoader} using the file system. @@ -31,6 +32,8 @@ public abstract class FileSystemInstanceLoader implements InstanceLoader { private final Map instanceSources; + private final Executor executor; + /** * Creates an {@link InstanceLoader} based on a file system. * @@ -38,16 +41,17 @@ public abstract class FileSystemInstanceLoader implements InstanceLoader { * @param chunkSupplier The {@link ChunkSupplier} used to define the chunk implementation used */ public FileSystemInstanceLoader(@NotNull InstanceManager instanceManager, @NotNull Path rootPath, - @NotNull ChunkSupplier chunkSupplier) { + @NotNull ChunkSupplier chunkSupplier, @NotNull Executor executor) { this.instanceManager = Objects.requireNonNull(instanceManager, "instanceManager"); this.rootPath = Objects.requireNonNull(rootPath, "rootPath"); this.chunkSupplier = Objects.requireNonNull(chunkSupplier, "chunkSupplier"); this.instanceSources = new ConcurrentHashMap<>(); + this.executor = Objects.requireNonNull(executor, "executorService"); } // TODO: what if there are distinct spawnPos invocations? @Override - public @NotNull Instance loadInstance(@UnmodifiableView @NotNull List subPaths) { + public @NotNull CompletableFuture loadInstance(@UnmodifiableView @NotNull List subPaths) { Path path = rootPath; for (String subPath : subPaths) { path = path.resolve(subPath); @@ -58,11 +62,13 @@ public FileSystemInstanceLoader(@NotNull InstanceManager instanceManager, @NotNu throw new IllegalArgumentException("Instance at " + path + " has not been preloaded"); } - InstanceContainer container = source.copy(); - container.setChunkSupplier(chunkSupplier); - instanceManager.registerInstance(container); + return CompletableFuture.supplyAsync(source::copy, executor).thenApply(container -> { + container.enableAutoChunkLoad(false); + container.setChunkSupplier(chunkSupplier); + instanceManager.registerInstance(container); - return container; + return container; + }); } @Override @@ -81,7 +87,9 @@ public void preload(@UnmodifiableView @NotNull List subPaths, @NotNull P @SuppressWarnings("UnstableApiUsage") private InstanceContainer createTemplateContainer(InstanceManager instanceManager, Path path, Point spawnPoint, int chunkViewDistance) { - InstanceContainer container = instanceManager.createInstanceContainer(createChunkLoader(path)); + InstanceContainer container = + new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, createChunkLoader(path)); + container.enableAutoChunkLoad(false); container.setChunkSupplier(chunkSupplier); awaitChunkLoadSync(container, spawnPoint, chunkViewDistance); @@ -93,7 +101,7 @@ private void awaitChunkLoadSync(Instance instance, Point spawnPoint, int chunkVi Phaser phaser = new Phaser(1); ChunkUtils.forChunksInRange(spawnPoint, chunkViewDistance, (chunkX, chunkZ) -> { phaser.register(); - instance.loadOptionalChunk(chunkX, chunkZ).whenComplete((chunk, throwable) -> phaser.arriveAndDeregister()); + instance.loadChunk(chunkX, chunkZ).whenComplete((chunk, throwable) -> phaser.arriveAndDeregister()); }); phaser.arriveAndAwaitAdvance(); } diff --git a/core/src/main/java/org/phantazm/core/instance/InstanceLoader.java b/core/src/main/java/org/phantazm/core/instance/InstanceLoader.java index 18f392c54..7b19601ec 100644 --- a/core/src/main/java/org/phantazm/core/instance/InstanceLoader.java +++ b/core/src/main/java/org/phantazm/core/instance/InstanceLoader.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.UnmodifiableView; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * Loads usable {@link Instance}s. @@ -18,7 +19,7 @@ public interface InstanceLoader { * @param subPaths Paths used to identify the {@link Instance} * @return A new {@link Instance} */ - @NotNull Instance loadInstance(@UnmodifiableView @NotNull List subPaths); + @NotNull CompletableFuture loadInstance(@UnmodifiableView @NotNull List subPaths); void preload(@UnmodifiableView @NotNull List subPaths, @NotNull Point spawnPos, int chunkViewDistance); diff --git a/core/src/main/java/org/phantazm/core/inventory/BasicInventoryAccessRegistry.java b/core/src/main/java/org/phantazm/core/inventory/BasicInventoryAccessRegistry.java index 7c0923008..f58ab290e 100644 --- a/core/src/main/java/org/phantazm/core/inventory/BasicInventoryAccessRegistry.java +++ b/core/src/main/java/org/phantazm/core/inventory/BasicInventoryAccessRegistry.java @@ -1,6 +1,7 @@ package org.phantazm.core.inventory; import net.kyori.adventure.key.Key; +import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.phantazm.core.equipment.Equipment; @@ -35,20 +36,20 @@ public void switchAccess(@Nullable Key key) { if (key == null) { applyTo(null); currentAccess = null; + return; } - else { - InventoryAccess access = accessMap.get(key); - if (access == null) { - throw new IllegalArgumentException("No matching inventory access found"); - } - if (access == currentAccess) { - return; - } + InventoryAccess access = accessMap.get(key); + if (access == null) { + throw new IllegalArgumentException("No matching inventory access found"); + } - currentAccess = access; - applyTo(access); + if (access == currentAccess) { + return; } + + applyTo(access); + currentAccess = access; } } @@ -67,51 +68,63 @@ public void registerAccess(@NotNull Key key, @NotNull InventoryAccess profile) { } @Override - public void unregisterAccess(@NotNull Key key) { - Objects.requireNonNull(key, "key"); + public boolean canPushTo(@NotNull InventoryAccess currentAccess, @NotNull Key groupKey) { + InventoryObjectGroup group = currentAccess.groups().get(groupKey); + return group != null && !group.isFull(); + } - synchronized (sync) { - if (!accessMap.containsKey(key)) { - throw new IllegalArgumentException("Inventory profile not yet registered"); - } + @Override + public @Nullable InventoryObject replaceObject(@NotNull InventoryAccess access, int slot, + @NotNull InventoryObject newObject) { + InventoryProfile profile = access.profile(); + InventoryObject old = null; + if (profile.hasInventoryObject(slot)) { + old = profile.removeInventoryObject(slot); - accessMap.remove(key); + if (access == currentAccess) { + old.end(); + } } - } - @Override - public boolean canPushTo(@NotNull Key groupKey) { - Optional accessOptional = getCurrentAccess(); - if (accessOptional.isPresent()) { - InventoryAccess inventoryAccess = accessOptional.get(); - InventoryObjectGroup group = inventoryAccess.groups().get(groupKey); - return group != null && !group.isFull(); + profile.setInventoryObject(slot, newObject); + + if (access == currentAccess) { + onAdd(slot, newObject); } - return false; + return old; } @Override - public void replaceObject(int slot, @NotNull InventoryObject newObject) { - InventoryAccess access = getAccess(); - + public @Nullable InventoryObject removeObject(@NotNull InventoryAccess access, int slot) { InventoryProfile profile = access.profile(); + + InventoryObject old = null; if (profile.hasInventoryObject(slot)) { - InventoryObject old = profile.removeInventoryObject(slot); - old.end(); + old = profile.removeInventoryObject(slot); + + if (access == this.currentAccess) { + old.end(); + playerView.getPlayer().ifPresent(player -> player.getInventory().setItemStack(slot, ItemStack.AIR)); + } } - - profile.setInventoryObject(slot, newObject); - onAdd(slot, newObject); + + return old; } @Override - public void pushObject(@NotNull Key groupKey, @NotNull InventoryObject object) { - InventoryAccess access = getAccess(); + public void pushObject(@NotNull InventoryAccess access, @NotNull Key groupKey, @NotNull InventoryObject object) { InventoryObjectGroup group = getGroup(access, groupKey); int slot = group.pushInventoryObject(object); - onAdd(slot, object); + if (access == currentAccess) { + onAdd(slot, object); + } + } + + @Override + public @NotNull InventoryAccess getAccess(@NotNull Key profileKey) { + return Objects.requireNonNull(accessMap.get(profileKey), "no access named " + profileKey); } private void onAdd(int slot, InventoryObject object) { @@ -123,15 +136,6 @@ private void onAdd(int slot, InventoryObject object) { }); } - private InventoryAccess getAccess() { - Optional accessOptional = getCurrentAccess(); - if (accessOptional.isEmpty()) { - throw new IllegalArgumentException("No current access"); - } - - return accessOptional.get(); - } - private InventoryObjectGroup getGroup(InventoryAccess access, Key groupKey) { InventoryObjectGroup group = access.groups().get(groupKey); if (group == null) { @@ -145,18 +149,22 @@ private void applyTo(InventoryAccess newAccess) { playerView.getPlayer().ifPresent(player -> { player.getInventory().clear(); - InventoryProfile oldProfile = this.currentAccess.profile(); - for (int slot = 0; slot < oldProfile.getSlotCount(); slot++) { - if (!oldProfile.hasInventoryObject(slot)) { - continue; - } + InventoryAccess oldAccess = this.currentAccess; - InventoryObject object = oldProfile.getInventoryObject(slot); - if (slot == player.getHeldSlot() && object instanceof Equipment equipment) { - equipment.setSelected(false); - } + if (oldAccess != null) { + InventoryProfile oldProfile = oldAccess.profile(); + for (int slot = 0; slot < oldProfile.getSlotCount(); slot++) { + if (!oldProfile.hasInventoryObject(slot)) { + continue; + } + + InventoryObject object = oldProfile.getInventoryObject(slot); + if (slot == player.getHeldSlot() && object instanceof Equipment equipment) { + equipment.setSelected(false); + } - object.end(); + object.end(); + } } if (newAccess != null) { diff --git a/core/src/main/java/org/phantazm/core/inventory/InventoryAccessRegistry.java b/core/src/main/java/org/phantazm/core/inventory/InventoryAccessRegistry.java index 8d2147b13..067f71c75 100644 --- a/core/src/main/java/org/phantazm/core/inventory/InventoryAccessRegistry.java +++ b/core/src/main/java/org/phantazm/core/inventory/InventoryAccessRegistry.java @@ -35,17 +35,31 @@ public interface InventoryAccessRegistry { */ void registerAccess(@NotNull Key key, @NotNull InventoryAccess profile); - /** - * Unregisters a {@link InventoryProfile} from the view. - * - * @param key The {@link Key} of the {@link InventoryProfile} to unregister - * @throws IllegalArgumentException If no {@link InventoryProfile} is registered with the {@link Key} - */ - void unregisterAccess(@NotNull Key key); + default boolean canPushTo(@NotNull Key groupKey) { + return getCurrentAccess().filter(access -> canPushTo(access, groupKey)).isPresent(); + } + + boolean canPushTo(@NotNull InventoryAccess currentAccess, @NotNull Key groupKey); + + default @Nullable InventoryObject replaceObject(int slot, @NotNull InventoryObject newObject) { + return getCurrentAccess().map(access -> replaceObject(access, slot, newObject)).orElse(null); + + } + + @Nullable InventoryObject replaceObject(@NotNull InventoryAccess currentAccess, int slot, + @NotNull InventoryObject newObject); + + default @Nullable InventoryObject removeObject(int slot) { + return getCurrentAccess().map(access -> removeObject(access, slot)).orElse(null); + } + + @Nullable InventoryObject removeObject(@NotNull InventoryAccess currentAccess, int slot); - boolean canPushTo(@NotNull Key groupKey); + default void pushObject(@NotNull Key groupKey, @NotNull InventoryObject object) { + getCurrentAccess().ifPresent(access -> pushObject(access, groupKey, object)); + } - void replaceObject(int slot, @NotNull InventoryObject newObject); + void pushObject(@NotNull InventoryAccess currentAccess, @NotNull Key groupKey, @NotNull InventoryObject object); - void pushObject(@NotNull Key groupKey, @NotNull InventoryObject object); + @NotNull InventoryAccess getAccess(@NotNull Key profileKey); } diff --git a/core/src/main/java/org/phantazm/core/item/StaticUpdatingItem.java b/core/src/main/java/org/phantazm/core/item/StaticUpdatingItem.java index 69da81be0..b87e7454c 100644 --- a/core/src/main/java/org/phantazm/core/item/StaticUpdatingItem.java +++ b/core/src/main/java/org/phantazm/core/item/StaticUpdatingItem.java @@ -1,5 +1,6 @@ package org.phantazm.core.item; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -9,6 +10,7 @@ import java.util.Objects; @Model("item.updating.static") +@Cache public class StaticUpdatingItem implements UpdatingItem { private final Data data; diff --git a/core/src/main/java/org/phantazm/core/npc/NPCHandler.java b/core/src/main/java/org/phantazm/core/npc/NPCHandler.java index 589350e2c..12257d1cf 100644 --- a/core/src/main/java/org/phantazm/core/npc/NPCHandler.java +++ b/core/src/main/java/org/phantazm/core/npc/NPCHandler.java @@ -1,7 +1,7 @@ package org.phantazm.core.npc; import net.minestom.server.entity.Entity; -import net.minestom.server.event.EventFilter; +import net.minestom.server.entity.Player; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.PlayerEntityInteractEvent; import net.minestom.server.event.trait.InstanceEvent; @@ -13,24 +13,24 @@ import java.util.Objects; public class NPCHandler implements Tickable { - private final Instance instance; private final List npcs; - private final EventNode instanceEventNode; - private final EventNode handlerNode; + private final Instance instance; + private final EventNode instanceNode; - public NPCHandler(@NotNull List npcs, @NotNull Instance instance) { - this.instance = Objects.requireNonNull(instance, "instance"); + public NPCHandler(@NotNull List npcs, @NotNull Instance instance, + @NotNull EventNode instanceNode) { this.npcs = List.copyOf(npcs); - this.instanceEventNode = instance.eventNode(); - - this.handlerNode = EventNode.event("npc_handler_{" + instance.getUniqueId() + "}", - EventFilter.from(InstanceEvent.class, Instance.class, InstanceEvent::getInstance), event -> true); + this.instance = Objects.requireNonNull(instance, "instance"); + this.instanceNode = Objects.requireNonNull(instanceNode, "instanceNode"); - handlerNode.addListener(PlayerEntityInteractEvent.class, this::entityInteractEvent); - instanceEventNode.addChild(handlerNode); + instanceNode.addListener(PlayerEntityInteractEvent.class, this::entityInteractEvent); } private void entityInteractEvent(PlayerEntityInteractEvent event) { + if (event.getHand() != Player.Hand.MAIN) { + return; + } + if (event.getInstance() != instance) { return; } @@ -53,8 +53,6 @@ public void end() { for (NPC npc : npcs) { npc.despawn(); } - - instanceEventNode.removeChild(handlerNode); } @Override @@ -63,4 +61,8 @@ public void tick(long time) { npc.tick(time); } } + + public @NotNull EventNode instanceNode() { + return instanceNode; + } } diff --git a/core/src/main/java/org/phantazm/core/npc/interactor/MessageInteractor.java b/core/src/main/java/org/phantazm/core/npc/interactor/MessageInteractor.java new file mode 100644 index 000000000..0656850de --- /dev/null +++ b/core/src/main/java/org/phantazm/core/npc/interactor/MessageInteractor.java @@ -0,0 +1,48 @@ +package org.phantazm.core.npc.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; + +@Model("npc.interactor.message") +@Cache +public class MessageInteractor implements Interactor { + private final Data data; + + @FactoryMethod + public MessageInteractor(@NotNull Data data) { + this.data = data; + } + + @Override + public void interact(@NotNull Player player) { + if (data.broadcast) { + Instance instance = player.getInstance(); + if (instance != null) { + instance.sendMessage(data.message); + } + else { + player.sendMessage(data.message); + } + } + else { + player.sendMessage(data.message); + } + } + + @DataObject + public record Data(@NotNull Component message, boolean broadcast) { + @Default("broadcast") + public static @NotNull ConfigElement broadcastDefault() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/core/src/main/java/org/phantazm/core/npc/interactor/ShowGuiInteractor.java b/core/src/main/java/org/phantazm/core/npc/interactor/ShowGuiInteractor.java new file mode 100644 index 000000000..9e89892ee --- /dev/null +++ b/core/src/main/java/org/phantazm/core/npc/interactor/ShowGuiInteractor.java @@ -0,0 +1,48 @@ +package org.phantazm.core.npc.interactor; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.gui.BasicSlotDistributor; +import org.phantazm.core.gui.Gui; +import org.phantazm.core.gui.GuiItem; + +import java.util.List; + +@Model("npc.interactor.show_gui") +@Cache(false) +public class ShowGuiInteractor implements Interactor { + private final Data data; + private final List guiItems; + + @FactoryMethod + public ShowGuiInteractor(@NotNull Data data, @NotNull @Child("gui_items") List guiItems) { + this.data = data; + this.guiItems = guiItems; + } + + @Override + public void interact(@NotNull Player player) { + player.openInventory( + Gui.builder(data.inventoryType, new BasicSlotDistributor(data.padding)).withItems(guiItems).build()); + } + + @DataObject + public record Data(@NotNull InventoryType inventoryType, + int padding, + @NotNull @ChildPath("gui_items") List guiItems) { + @Default("inventoryType") + public static @NotNull ConfigElement defaultInventoryType() { + return ConfigPrimitive.of("CHEST_3_ROW"); + } + + @Default("padding") + public static @NotNull ConfigElement defaultPadding() { + return ConfigPrimitive.of(1); + } + } +} diff --git a/core/src/main/java/org/phantazm/core/npc/interactor/item/InteractorDelegatingItem.java b/core/src/main/java/org/phantazm/core/npc/interactor/item/InteractorDelegatingItem.java new file mode 100644 index 000000000..fb569fb94 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/npc/interactor/item/InteractorDelegatingItem.java @@ -0,0 +1,66 @@ +package org.phantazm.core.npc.interactor.item; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.gui.Gui; +import org.phantazm.core.gui.GuiItem; +import org.phantazm.core.npc.interactor.Interactor; + +@Model("npc.interactor.gui.item") +@Cache(false) +public class InteractorDelegatingItem implements GuiItem { + private final Data data; + private final Interactor interactor; + + @FactoryMethod + public InteractorDelegatingItem(@NotNull Data data, @NotNull @Child("interactor") Interactor interactor) { + this.data = data; + this.interactor = interactor; + } + + @Override + public void handleClick(@NotNull Gui owner, @NotNull Player player, int slot, @NotNull ClickType clickType) { + if (clickType == ClickType.LEFT_CLICK) { + interactor.interact(player); + + if (data.closeOnClick) { + player.closeInventory(); + } + } + } + + @Override + public void onRemove(@NotNull Gui owner, int slot) { + + } + + @Override + public void onReplace(@NotNull Gui owner, @NotNull GuiItem newItem, int slot) { + + } + + @Override + public @NotNull ItemStack getItemStack() { + return data.itemStack; + } + + @Override + public boolean shouldRedraw() { + return false; + } + + @DataObject + public record Data(@NotNull ItemStack itemStack, + boolean closeOnClick, + @NotNull @ChildPath("interactor") String interactor) { + @Default("closeOnClick") + public static @NotNull ConfigElement defaultCloseOnClick() { + return ConfigPrimitive.of(true); + } + } +} diff --git a/core/src/main/java/org/phantazm/core/npc/supplier/PlayerEntitySupplier.java b/core/src/main/java/org/phantazm/core/npc/supplier/PlayerEntitySupplier.java index cb32f3b1b..7e82064fe 100644 --- a/core/src/main/java/org/phantazm/core/npc/supplier/PlayerEntitySupplier.java +++ b/core/src/main/java/org/phantazm/core/npc/supplier/PlayerEntitySupplier.java @@ -11,6 +11,7 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.PlayerSkin; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.phantazm.core.entity.fakeplayer.MinimalFakePlayer; import java.util.function.Supplier; @@ -27,15 +28,29 @@ public PlayerEntitySupplier(@NotNull Data data) { @Override public Entity get() { - return new MinimalFakePlayer(MinecraftServer.getSchedulerManager(), data.playerName, - PlayerSkin.fromUuid(data.skinUUID.replace("-", ""))); + if (data.skinUUID != null) { + return new MinimalFakePlayer(MinecraftServer.getSchedulerManager(), data.playerName, + PlayerSkin.fromUuid(data.skinUUID.replace("-", ""))); + } + + return new MinimalFakePlayer(MinecraftServer.getSchedulerManager(), data.playerName, data.playerSkin); } @DataObject - public record Data(@NotNull String playerName, @NotNull String skinUUID) { + public record Data(@NotNull String playerName, @Nullable String skinUUID, @Nullable PlayerSkin playerSkin) { @Default("playerName") public static ConfigElement defaultPlayerName() { return ConfigPrimitive.of(""); } + + @Default("skinUUID") + public static ConfigElement defaultSkinUUID() { + return ConfigPrimitive.NULL; + } + + @Default("playerSkin") + public static ConfigElement defaultPlayerSkin() { + return ConfigPrimitive.NULL; + } } } diff --git a/server/src/main/java/org/phantazm/server/packet/BinaryDataReader.java b/core/src/main/java/org/phantazm/core/packet/BinaryDataReader.java similarity index 96% rename from server/src/main/java/org/phantazm/server/packet/BinaryDataReader.java rename to core/src/main/java/org/phantazm/core/packet/BinaryDataReader.java index 048db64a5..d45641bf8 100644 --- a/server/src/main/java/org/phantazm/server/packet/BinaryDataReader.java +++ b/core/src/main/java/org/phantazm/core/packet/BinaryDataReader.java @@ -1,4 +1,4 @@ -package org.phantazm.server.packet; +package org.phantazm.core.packet; import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryWriter; diff --git a/server/src/main/java/org/phantazm/server/packet/BinaryDataWriter.java b/core/src/main/java/org/phantazm/core/packet/BinaryDataWriter.java similarity index 76% rename from server/src/main/java/org/phantazm/server/packet/BinaryDataWriter.java rename to core/src/main/java/org/phantazm/core/packet/BinaryDataWriter.java index ceee74737..46b667b0d 100644 --- a/server/src/main/java/org/phantazm/server/packet/BinaryDataWriter.java +++ b/core/src/main/java/org/phantazm/core/packet/BinaryDataWriter.java @@ -1,5 +1,6 @@ -package org.phantazm.server.packet; +package org.phantazm.core.packet; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.binary.BinaryWriter; import org.jetbrains.annotations.NotNull; import org.phantazm.messaging.serialization.DataReader; @@ -23,6 +24,10 @@ public BinaryDataWriter(@NotNull BinaryWriter binaryWriter) { this.binaryWriter = Objects.requireNonNull(binaryWriter, "binaryWriter"); } + public static @NotNull BinaryDataWriter fromNetworkBuffer(@NotNull NetworkBuffer networkBuffer) { + return new BinaryDataWriter(new BinaryWriter(Objects.requireNonNull(networkBuffer, "networkBuffer"))); + } + @Override public void writeByte(byte data) { binaryWriter.writeByte(data); diff --git a/core/src/main/java/org/phantazm/core/packet/MinestomPacketUtils.java b/core/src/main/java/org/phantazm/core/packet/MinestomPacketUtils.java new file mode 100644 index 000000000..3aec8f816 --- /dev/null +++ b/core/src/main/java/org/phantazm/core/packet/MinestomPacketUtils.java @@ -0,0 +1,15 @@ +package org.phantazm.core.packet; + +import net.minestom.server.network.NetworkBuffer; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.Packet; + +public class MinestomPacketUtils { + + public static byte @NotNull[] serialize(@NotNull Packet packet) { + return NetworkBuffer.makeArray(buffer -> { + packet.write(BinaryDataWriter.fromNetworkBuffer(buffer)); + }); + } + +} diff --git a/core/src/main/java/org/phantazm/core/player/BasicPlayerView.java b/core/src/main/java/org/phantazm/core/player/BasicPlayerView.java index 073964ca8..29ed51d36 100644 --- a/core/src/main/java/org/phantazm/core/player/BasicPlayerView.java +++ b/core/src/main/java/org/phantazm/core/player/BasicPlayerView.java @@ -126,6 +126,17 @@ private CompletableFuture getUsernameRequest() { return getUsername().thenApply(Component::text); } + @Override + public @NotNull Component getDisplayNameIfPresent() { + Optional cached = getPlayer().map(Player::getDisplayName); + if (cached.isPresent()) { + return cached.get(); + } + + return getUsernameIfCached().map(Component::text).orElseGet(() -> Component.text(getUUID().toString())); + + } + @Override public @NotNull Optional getDisplayNameIfCached() { return getPlayer().map(Player::getDisplayName).or(() -> getUsernameIfCached().map(Component::text)); diff --git a/core/src/main/java/org/phantazm/core/player/PlayerView.java b/core/src/main/java/org/phantazm/core/player/PlayerView.java index 01cc88855..9f7b33c33 100644 --- a/core/src/main/java/org/phantazm/core/player/PlayerView.java +++ b/core/src/main/java/org/phantazm/core/player/PlayerView.java @@ -41,6 +41,8 @@ public interface PlayerView { @NotNull CompletableFuture getDisplayName(); + @NotNull Component getDisplayNameIfPresent(); + @NotNull Optional getDisplayNameIfCached(); /** diff --git a/core/src/main/java/org/phantazm/core/sound/BasicSongPlayer.java b/core/src/main/java/org/phantazm/core/sound/BasicSongPlayer.java index 9a9a3e385..eda517ba1 100644 --- a/core/src/main/java/org/phantazm/core/sound/BasicSongPlayer.java +++ b/core/src/main/java/org/phantazm/core/sound/BasicSongPlayer.java @@ -3,6 +3,8 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.sound.Sound; import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.NotNull; import java.util.Deque; @@ -23,9 +25,29 @@ public void tick(long time) { } @Override - public @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Emitter emitter, @NotNull List notes, - boolean loop) { - SongImpl song = new SongImpl(audience, emitter, notes, loop); + public @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, @NotNull Sound.Emitter emitter, + @NotNull List notes, float volume, boolean loop) { + Objects.requireNonNull(audience, "audience"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(emitter, "emitter"); + Objects.requireNonNull(notes, "notes"); + + SongImpl song = new SongImpl(audience, emitter, source, null, notes, volume, loop); + if (song.nextNote != null) { + songDeque.add(song); + } + + return song; + } + + @Override + public @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, double x, double y, double z, + @NotNull List notes, float volume, boolean loop) { + Objects.requireNonNull(audience, "audience"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(notes, "notes"); + + SongImpl song = new SongImpl(audience, null, source, new Vec(x, y, z), notes, volume, loop); if (song.nextNote != null) { songDeque.add(song); } @@ -36,7 +58,10 @@ public void tick(long time) { private static class SongImpl implements Song { private final Audience audience; private final Sound.Emitter emitter; + private final Sound.Source source; + private final Point location; private final List notes; + private final float volume; private final boolean loop; private boolean stopped; @@ -46,10 +71,14 @@ private static class SongImpl implements Song { private Note nextNote; private long lastNoteTime; - private SongImpl(Audience audience, Sound.Emitter emitter, List notes, boolean loop) { + private SongImpl(Audience audience, Sound.Emitter emitter, Sound.Source source, Point location, + List notes, float volume, boolean loop) { this.audience = Objects.requireNonNull(audience, "audience"); - this.emitter = Objects.requireNonNull(emitter, "emitter"); + this.emitter = emitter; + this.source = source; + this.location = location; this.notes = List.copyOf(notes); + this.volume = volume; this.loop = loop; this.noteIndex = 0; @@ -97,7 +126,16 @@ private boolean tick(long time) { this.lastNoteTime = time; do { - this.audience.playSound(this.currentSound = nextNote.sound(), this.emitter); + if (this.emitter != null) { + this.audience.playSound( + this.currentSound = Sound.sound(nextNote.soundType(), source, volume, nextNote.pitch()), + this.emitter); + } + else { + this.audience.playSound( + this.currentSound = Sound.sound(nextNote.soundType(), source, volume, nextNote.pitch()), + location.x(), location.y(), location.z()); + } nextNoteIndex = ++this.noteIndex; if (nextNoteIndex < this.notes.size()) { diff --git a/core/src/main/java/org/phantazm/core/sound/NBSSongLoader.java b/core/src/main/java/org/phantazm/core/sound/NBSSongLoader.java index b29eb3e0e..a8744c346 100644 --- a/core/src/main/java/org/phantazm/core/sound/NBSSongLoader.java +++ b/core/src/main/java/org/phantazm/core/sound/NBSSongLoader.java @@ -3,7 +3,6 @@ import com.github.steanky.element.core.key.Constants; import com.github.steanky.element.core.key.KeyParser; import net.kyori.adventure.key.Key; -import net.kyori.adventure.sound.Sound; import org.apache.commons.lang3.StringUtils; import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.NotNull; @@ -267,8 +266,7 @@ private record NBSNote(int delayTick, int instrumentIndex, float normalizedPitch instrumentKey = customInstrumentKeys[index]; } - Sound sound = Sound.sound(instrumentKey, Sound.Source.MUSIC, 10, nbsNote.normalizedPitch); - actualNotes.add(new SongPlayer.Note(sound, nbsNote.delayTick)); + actualNotes.add(new SongPlayer.Note(instrumentKey, nbsNote.normalizedPitch, nbsNote.delayTick)); } return Optional.of(List.copyOf(actualNotes)); @@ -282,7 +280,7 @@ private record NBSNote(int delayTick, int instrumentIndex, float normalizedPitch } private static float normalizeKey(int key, float detune) { - float uses = (key + detune) - MIN_KEY; + double uses = (key + (double)detune) - MIN_KEY; return (float)Math.pow(2, (uses - 12) / 12D); } diff --git a/core/src/main/java/org/phantazm/core/sound/SongPlayer.java b/core/src/main/java/org/phantazm/core/sound/SongPlayer.java index 3755d220f..f3c4b517e 100644 --- a/core/src/main/java/org/phantazm/core/sound/SongPlayer.java +++ b/core/src/main/java/org/phantazm/core/sound/SongPlayer.java @@ -1,18 +1,39 @@ package org.phantazm.core.sound; import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Point; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Tickable; import java.util.List; public interface SongPlayer extends Tickable { - @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Emitter emitter, @NotNull List notes, - boolean loop); + @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, @NotNull Sound.Emitter emitter, + @NotNull List notes, float volume, boolean loop); - default @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Emitter emitter, @NotNull List notes) { - return play(audience, emitter, notes, false); + @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, double x, double y, double z, + @NotNull List notes, float volume, boolean loop); + + default @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, double x, double y, double z, + @NotNull List notes, float volume) { + return play(audience, source, x, y, z, notes, volume, false); + } + + default @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, @NotNull Point point, + @NotNull List notes, float volume) { + return play(audience, source, point.x(), point.y(), point.z(), notes, volume); + } + + default @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, @NotNull Sound.Emitter emitter, + @NotNull List notes, float volume) { + return play(audience, source, emitter, notes, volume, false); + } + + default @NotNull Song play(@NotNull Audience audience, @NotNull Sound.Source source, @NotNull Point point, + @NotNull List notes, float volume, boolean loop) { + return play(audience, source, point.x(), point.y(), point.z(), notes, volume, loop); } interface Song { @@ -21,6 +42,6 @@ interface Song { boolean isFinished(); } - record Note(@NotNull Sound sound, int ticks) { + record Note(@NotNull Key soundType, float pitch, int ticks) { } } diff --git a/core/src/main/java/org/phantazm/core/tracker/BoundedTrackerImpl.java b/core/src/main/java/org/phantazm/core/tracker/BoundedTrackerImpl.java index 6465efb04..298ac122b 100644 --- a/core/src/main/java/org/phantazm/core/tracker/BoundedTrackerImpl.java +++ b/core/src/main/java/org/phantazm/core/tracker/BoundedTrackerImpl.java @@ -62,11 +62,11 @@ private static Long2ObjectMap chunkItems(Collectio @Override public @NotNull Optional closestInRangeToBounds(@NotNull Point origin, double width, double height, double depth, double distance) { - int startX = (int)Math.floor(origin.x() - distance) >> 4; - int startZ = (int)Math.floor(origin.z() - distance) >> 4; + int startX = (int)Math.floor(origin.x() - (distance + (width / 2))) >> 4; + int startZ = (int)Math.floor(origin.z() - (distance + (depth / 2))) >> 4; - int endX = (int)Math.floor(origin.x() + distance - Vec.EPSILON) >> 4; - int endZ = (int)Math.floor(origin.z() + distance - Vec.EPSILON) >> 4; + int endX = (int)Math.floor(origin.x() + (distance + (width / 2)) - Vec.EPSILON) >> 4; + int endZ = (int)Math.floor(origin.z() + (distance + (depth / 2)) - Vec.EPSILON) >> 4; T closest = null; double closestDistance = Double.POSITIVE_INFINITY; diff --git a/core/src/test/java/org/phantazm/core/game/scene/SceneProviderAbstractTest.java b/core/src/test/java/org/phantazm/core/game/scene/SceneProviderAbstractTest.java index 4679549b3..a2b3fc173 100644 --- a/core/src/test/java/org/phantazm/core/game/scene/SceneProviderAbstractTest.java +++ b/core/src/test/java/org/phantazm/core/game/scene/SceneProviderAbstractTest.java @@ -1,6 +1,8 @@ package org.phantazm.core.game.scene; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -8,6 +10,9 @@ import java.util.Collection; import java.util.Iterator; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -17,10 +22,17 @@ public class SceneProviderAbstractTest { private final int maximumLobbies = 10; + private ExecutorService executor; + + @BeforeEach + public void setup() { + executor = Executors.newSingleThreadExecutor(); + } + @Test public void testMaximumLobbies() { SceneProvider, SceneJoinRequest> sceneProvider = - new SceneProviderAbstract<>(maximumLobbies) { + new SceneProviderAbstract<>(executor,maximumLobbies) { @Override protected @NotNull Optional> chooseScene(@NotNull SceneJoinRequest o) { return Optional.empty(); @@ -28,8 +40,8 @@ public void testMaximumLobbies() { @SuppressWarnings("unchecked") @Override - protected @NotNull Scene createScene(@NotNull SceneJoinRequest o) { - return (Scene)mock(Scene.class); + protected @NotNull CompletableFuture> createScene(@NotNull SceneJoinRequest o) { + return CompletableFuture.completedFuture((Scene)mock(Scene.class)); } @Override @@ -40,9 +52,9 @@ protected void cleanupScene(@NotNull Scene scene) { SceneJoinRequest request = Mockito.mock(SceneJoinRequest.class); for (int i = 0; i < maximumLobbies; i++) { - assertTrue(sceneProvider.provideScene(request).isPresent()); + assertTrue(sceneProvider.provideScene(request).join().isPresent()); } - assertTrue(sceneProvider.provideScene(request).isEmpty()); + assertTrue(sceneProvider.provideScene(request).join().isEmpty()); } @SuppressWarnings("unchecked") @@ -54,7 +66,7 @@ public void testProviderScenesAreTicked() { } SceneProvider, SceneJoinRequest> sceneProvider = - new SceneProviderAbstract<>(maximumLobbies) { + new SceneProviderAbstract<>(executor,maximumLobbies) { private final Iterator> iterator = scenes.iterator(); @@ -64,8 +76,8 @@ public void testProviderScenesAreTicked() { } @Override - protected @NotNull Scene createScene(@NotNull SceneJoinRequest o) { - return iterator.next(); + protected @NotNull CompletableFuture> createScene(@NotNull SceneJoinRequest o) { + return CompletableFuture.completedFuture(iterator.next()); } @Override @@ -77,7 +89,7 @@ protected void cleanupScene(@NotNull Scene scene) { SceneJoinRequest request = Mockito.mock(SceneJoinRequest.class); for (int i = 0; i < maximumLobbies; i++) { - sceneProvider.provideScene(request); + sceneProvider.provideScene(request).join(); } sceneProvider.tick(0); @@ -93,15 +105,15 @@ public void testProviderRemovesShutdownScenes() { when(scene.isShutdown()).thenReturn(true); SceneProvider, SceneJoinRequest> sceneProvider = - new SceneProviderAbstract<>(maximumLobbies) { + new SceneProviderAbstract<>(executor, maximumLobbies) { @Override protected @NotNull Optional> chooseScene(@NotNull SceneJoinRequest o) { return Optional.empty(); } @Override - protected @NotNull Scene createScene(@NotNull SceneJoinRequest o) { - return scene; + protected @NotNull CompletableFuture> createScene(@NotNull SceneJoinRequest o) { + return CompletableFuture.completedFuture(scene); } @Override @@ -111,11 +123,16 @@ protected void cleanupScene(@NotNull Scene scene) { }; SceneJoinRequest request = Mockito.mock(SceneJoinRequest.class); - sceneProvider.provideScene(request); + sceneProvider.provideScene(request).join(); sceneProvider.tick(0); assertFalse(sceneProvider.getScenes().iterator().hasNext()); verify(scene, never()).tick(0); } + @AfterEach + public void tearDown() { + executor.shutdownNow(); + } + } diff --git a/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyIntegrationTest.java b/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyIntegrationTest.java index 873a48999..b95d17bac 100644 --- a/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyIntegrationTest.java @@ -1,11 +1,9 @@ package org.phantazm.core.game.scene.lobby; -import net.kyori.adventure.text.Component; -import net.minestom.server.entity.Player; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.instance.Instance; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.phantazm.core.config.InstanceConfig; import org.phantazm.core.game.scene.TransferResult; @@ -15,11 +13,11 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @EnvTest @@ -32,16 +30,16 @@ public void testShutdown(Env env) { Instance instance = env.createFlatInstance(); InstanceConfig instanceConfig = new InstanceConfig(InstanceConfig.DEFAULT_POS, InstanceConfig.DEFAULT_TIME, InstanceConfig.DEFAULT_TIME_RATE, InstanceConfig.DEFAULT_CHUNK_LOAD_RANGE); - SceneFallback sceneFallback = (ignored) -> true; + SceneFallback sceneFallback = (ignored) -> CompletableFuture.completedFuture(true); Lobby lobby = new Lobby(UUID.randomUUID(), instance, instanceConfig, sceneFallback, - new NPCHandler(List.of(), instance), true); + new NPCHandler(List.of(), instance, instance.eventNode()), Collections.emptyList(), + MiniMessage.miniMessage(), "", true); PlayerView playerView = mock(PlayerView.class); lobby.shutdown(); assertTrue(lobby.isShutdown()); - TransferResult result = - lobby.join(new BasicLobbyJoinRequest(env.process().connection(), Collections.singleton(playerView))); + TransferResult result = lobby.join(new BasicLobbyJoinRequest(Collections.singleton(playerView))); assertFalse(result.executor().isPresent()); } diff --git a/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyRouterIntegrationTest.java b/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyRouterIntegrationTest.java index ae4a666de..f0e0999b6 100644 --- a/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyRouterIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/game/scene/lobby/LobbyRouterIntegrationTest.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -26,10 +25,9 @@ public void testJoinRejectionWhenNotJoinable(Env env) { SceneRouter router = new LobbyRouter(sceneProviders); router.setJoinable(false); - LobbyRouteRequest request = - new LobbyRouteRequest(lobbyName, new BasicLobbyJoinRequest(env.process().connection(), List.of())); + LobbyRouteRequest request = new LobbyRouteRequest(lobbyName, new BasicLobbyJoinRequest(List.of())); - assertTrue(router.findScene(request).scene().isEmpty()); + assertTrue(router.findScene(request).join().scene().isEmpty()); } @SuppressWarnings("unchecked") @@ -41,10 +39,9 @@ public void testJoinRejectionWhenShutdown(Env env) { SceneRouter router = new LobbyRouter(sceneProviders); router.shutdown(); - LobbyRouteRequest request = - new LobbyRouteRequest(lobbyName, new BasicLobbyJoinRequest(env.process().connection(), List.of())); + LobbyRouteRequest request = new LobbyRouteRequest(lobbyName, new BasicLobbyJoinRequest(List.of())); - assertTrue(router.findScene(request).scene().isEmpty()); + assertTrue(router.findScene(request).join().scene().isEmpty()); } @SuppressWarnings("unchecked") @@ -55,10 +52,9 @@ public void testJoinRequestWithNoMatchingSceneProvider(Env env) { Collections.singletonMap(lobbyName, (SceneProvider)mock(SceneProvider.class)); SceneRouter router = new LobbyRouter(sceneProviders); - LobbyRouteRequest request = - new LobbyRouteRequest("notMain", new BasicLobbyJoinRequest(env.process().connection(), List.of())); + LobbyRouteRequest request = new LobbyRouteRequest("notMain", new BasicLobbyJoinRequest(List.of())); - assertTrue(router.findScene(request).scene().isEmpty()); + assertTrue(router.findScene(request).join().scene().isEmpty()); } } diff --git a/core/src/test/java/org/phantazm/core/guild/party/command/PartyCreateCommandIntegrationTest.java b/core/src/test/java/org/phantazm/core/guild/party/command/PartyCreateCommandIntegrationTest.java index d5955d540..4a1159f5d 100644 --- a/core/src/test/java/org/phantazm/core/guild/party/command/PartyCreateCommandIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/guild/party/command/PartyCreateCommandIntegrationTest.java @@ -24,9 +24,9 @@ public class PartyCreateCommandIntegrationTest extends AbstractPartyCommandInteg @Test public void testCreateCreatesParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, - partyCreator, new Random()); + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance, Pos.ZERO); @@ -43,9 +43,9 @@ public void testCreateCreatesParty(Env env) { @Test public void testCreateDoesNotCreatePartyIfAlreadyInParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, - partyCreator, new Random()); + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance, Pos.ZERO); diff --git a/core/src/test/java/org/phantazm/core/guild/party/command/PartyInviteCommandIntegrationTest.java b/core/src/test/java/org/phantazm/core/guild/party/command/PartyInviteCommandIntegrationTest.java index b228215be..bd58a4e33 100644 --- a/core/src/test/java/org/phantazm/core/guild/party/command/PartyInviteCommandIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/guild/party/command/PartyInviteCommandIntegrationTest.java @@ -24,8 +24,9 @@ public class PartyInviteCommandIntegrationTest extends AbstractPartyCommandInteg @Test public void testCanInviteWithInvitePermission(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -44,8 +45,9 @@ public void testCanInviteWithInvitePermission(Env env) { @Test public void testCanInviteOtherPlayerInPartyWithInvitePermission(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -65,8 +67,9 @@ public void testCanInviteOtherPlayerInPartyWithInvitePermission(Env env) { @Test public void testCannotInviteWithoutInvitePermission(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().setMinimumInviteRank(2).build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setMinimumInviteRank(2).setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -85,8 +88,9 @@ public void testCannotInviteWithoutInvitePermission(Env env) { @Test public void testCannotInviteSelf(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance, Pos.ZERO); @@ -102,8 +106,9 @@ public void testCannotInviteSelf(Env env) { @Test public void testPartyCreatedOnInviteWithoutAlreadyBeingInParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); diff --git a/core/src/test/java/org/phantazm/core/guild/party/command/PartyJoinCommandIntegrationTest.java b/core/src/test/java/org/phantazm/core/guild/party/command/PartyJoinCommandIntegrationTest.java index 297d61ac8..71f557205 100644 --- a/core/src/test/java/org/phantazm/core/guild/party/command/PartyJoinCommandIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/guild/party/command/PartyJoinCommandIntegrationTest.java @@ -24,8 +24,9 @@ public class PartyJoinCommandIntegrationTest extends AbstractPartyCommandIntegra @Test public void testNotInPartyAfterJoiningOtherPlayerNotInParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -43,8 +44,9 @@ public void testNotInPartyAfterJoiningOtherPlayerNotInParty(Env env) { @Test public void testNotInPartyWithoutInviteAndJoiningOtherPlayerInParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -64,8 +66,9 @@ public void testNotInPartyWithoutInviteAndJoiningOtherPlayerInParty(Env env) { @Test public void testInPartyAfterInviteAndJoiningOtherPlayerInParty(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); diff --git a/core/src/test/java/org/phantazm/core/guild/party/command/PartyKickCommandIntegrationTest.java b/core/src/test/java/org/phantazm/core/guild/party/command/PartyKickCommandIntegrationTest.java index 736189d4c..1a2ab6a35 100644 --- a/core/src/test/java/org/phantazm/core/guild/party/command/PartyKickCommandIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/guild/party/command/PartyKickCommandIntegrationTest.java @@ -22,8 +22,9 @@ public class PartyKickCommandIntegrationTest extends AbstractPartyCommandIntegra @Test public void testCanKickWithSufficientRankAndGreaterRankThanTarget(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -45,8 +46,9 @@ public void testCanKickWithSufficientRankAndGreaterRankThanTarget(Env env) { @Test public void testCannotKickWithoutSufficientRankAndGreaterRankThanTarget(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().setMinimumKickRank(2).build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setMinimumKickRank(2).setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -68,8 +70,9 @@ public void testCannotKickWithoutSufficientRankAndGreaterRankThanTarget(Env env) @Test public void testCannotKickWithSufficientRankAndEqualRankToTarget(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().setDefaultRank(2).build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setDefaultRank(2).setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -92,8 +95,9 @@ public void testCannotKickWithSufficientRankAndEqualRankToTarget(Env env) { @Test public void testCannotKickSelf(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().setDefaultRank(2).build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setDefaultRank(2).setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, + partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance, Pos.ZERO); diff --git a/core/src/test/java/org/phantazm/core/guild/party/command/PartyLeaveCommandIntegrationTest.java b/core/src/test/java/org/phantazm/core/guild/party/command/PartyLeaveCommandIntegrationTest.java index c842b8285..0650f1310 100644 --- a/core/src/test/java/org/phantazm/core/guild/party/command/PartyLeaveCommandIntegrationTest.java +++ b/core/src/test/java/org/phantazm/core/guild/party/command/PartyLeaveCommandIntegrationTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import org.phantazm.core.guild.party.Party; import org.phantazm.core.guild.party.PartyCreator; +import org.phantazm.core.guild.party.PartyMember; import org.phantazm.core.player.BasicPlayerViewProvider; import org.phantazm.core.player.PlayerViewProvider; @@ -24,8 +25,9 @@ public class PartyLeaveCommandIntegrationTest extends AbstractPartyCommandIntegr @Test public void testNotInPartyAfterLeavingAndOwnerIsNullNoOtherMembers(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, + viewProvider, partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance, Pos.ZERO); @@ -41,10 +43,11 @@ public void testNotInPartyAfterLeavingAndOwnerIsNullNoOtherMembers(Env env) { @SuppressWarnings({"UnstableApiUsage", "JUnitMalformedDeclaration"}) @Test - public void testOwnerIsNotNullAfterLeavingWithOtherMembers(Env env) { + public void testOwnerIsNotNullAfterLeavingWithOtherMembersAndNewOwnerHasRankOfFormerOwner(Env env) { PlayerViewProvider viewProvider = new BasicPlayerViewProvider(identitySource, env.process().connection()); - PartyCreator partyCreator = new PartyCreator.Builder().build(); - Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, viewProvider, partyCreator, new Random()); + PartyCreator partyCreator = new PartyCreator.Builder().setCreatorRank(1).build(); + Command command = PartyCommand.partyCommand(commandConfig, MiniMessage.miniMessage(), partyHolder, + viewProvider, partyCreator, new Random(), 1); env.process().command().register(command); Instance instance = env.createFlatInstance(); Player firstPlayer = env.createPlayer(instance, Pos.ZERO); @@ -52,6 +55,7 @@ public void testOwnerIsNotNullAfterLeavingWithOtherMembers(Env env) { env.process().command().execute(firstPlayer, "party create"); Player secondPlayer = env.createPlayer(instance, Pos.ZERO); Party party = partyHolder.uuidToGuild().get(firstPlayer.getUuid()); + int oldRank = party.getOwner().get().rank(); secondPlayer.setUsernameField("second"); env.process().command().execute(firstPlayer, "party invite second"); env.process().command().execute(secondPlayer, "party join first"); @@ -60,7 +64,9 @@ public void testOwnerIsNotNullAfterLeavingWithOtherMembers(Env env) { assertFalse(partyHolder.uuidToGuild().containsKey(firstPlayer.getUuid())); assertFalse(party.getMemberManager().hasMember(firstPlayer.getUuid())); - assertNotNull(party.getOwner().get()); + PartyMember owner = party.getOwner().get(); + assertNotNull(owner); + assertEquals(oldRank, owner.rank()); assertNotEquals(firstPlayer.getUuid(), party.getOwner().get().getPlayerView().getUUID()); } diff --git a/defaultRunData/server-1/chat-config.yml b/defaultRunData/server-1/chat-config.yml new file mode 100644 index 000000000..c0798e360 --- /dev/null +++ b/defaultRunData/server-1/chat-config.yml @@ -0,0 +1,5 @@ +chatFormats: + - key: all + value: <> + - key: party + value: Party > <> diff --git a/defaultRunData/server-1/zombies.hikari.properties b/defaultRunData/server-1/hikari.properties similarity index 55% rename from defaultRunData/server-1/zombies.hikari.properties rename to defaultRunData/server-1/hikari.properties index 6e2cae748..a8599a700 100644 --- a/defaultRunData/server-1/zombies.hikari.properties +++ b/defaultRunData/server-1/hikari.properties @@ -1,2 +1,2 @@ dataSourceClassName=org.sqlite.SQLiteDataSource -dataSource.url=jdbc:sqlite:zombies.db +dataSource.url=jdbc:sqlite:phantazm.db diff --git a/defaultRunData/server-1/lobbies-config.yml b/defaultRunData/server-1/lobbies-config.yml index 27ed3d79e..6ce39fd7f 100644 --- a/defaultRunData/server-1/lobbies-config.yml +++ b/defaultRunData/server-1/lobbies-config.yml @@ -7,6 +7,8 @@ lobbies: lobbyPaths: [ "main" ] maxPlayers: 10 maxLobbies: 10 + lobbyJoinFormat: <#fffa9e>» joined the lobby + defaultItems: [] instanceConfig: time: 18000 diff --git a/defaultRunData/server-1/party.toml b/defaultRunData/server-1/party-config.toml similarity index 92% rename from defaultRunData/server-1/party.toml rename to defaultRunData/server-1/party-config.toml index 90f5e273f..b2515e9bd 100644 --- a/defaultRunData/server-1/party.toml +++ b/defaultRunData/server-1/party-config.toml @@ -12,7 +12,7 @@ decimalPlaces = 1 [notificationConfig] joinToPartyFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>has joined the party.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" joinToJoinerFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>Joined <#c0c9ed>'s <#9096ad>party.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" -inviteToPartyFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>has invited <#9096ad>to the party. They have <#b8bfde>s to accept.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" +inviteToPartyFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>has invited <#9096ad>to the party. They have s to accept.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" inviteToInviteeFromOwnerFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>has invited you to join their party. Click '>HERE to accept.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" inviteToInviteeFromOtherFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>has invited you to join <#c0c9ed>'s <#9096ad>party. Click '>HERE to accept.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" expiryToPartyFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad>The invitation to has expired.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" @@ -40,3 +40,6 @@ noInvite = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ toKickNotInPartyFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» isn't in your party!
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" cannotKickSelf = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» You can't kick yourself!
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" cannotKickOtherFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» You can't kick !
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" +listFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» Party Owner:

<#9096ad>Party Members <#b8bfde>():
<#b8bfde>

<#9096ad>Online<#b8bfde>:
<#9096ad>Offline<#b8bfde>:
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" +onlineMemberFormat = "" +offlineMemberFormat = " " diff --git a/defaultRunData/server-1/zombies.db b/defaultRunData/server-1/phantazm.db similarity index 57% rename from defaultRunData/server-1/zombies.db rename to defaultRunData/server-1/phantazm.db index 1d0665356..f08e13877 100644 Binary files a/defaultRunData/server-1/zombies.db and b/defaultRunData/server-1/phantazm.db differ diff --git a/defaultRunData/server-1/player-config.toml b/defaultRunData/server-1/player-config.toml new file mode 100644 index 000000000..79481f836 --- /dev/null +++ b/defaultRunData/server-1/player-config.toml @@ -0,0 +1,2 @@ +nameFormat = "" +joinMessage = "Welcome to Phantazm." diff --git a/defaultRunData/server-1/server-config.toml b/defaultRunData/server-1/server-config.toml index f0dde4e7b..1c7771138 100644 --- a/defaultRunData/server-1/server-config.toml +++ b/defaultRunData/server-1/server-config.toml @@ -1,7 +1,6 @@ [serverInfo] serverIP = "0.0.0.0" port = 25567 -optifineEnabled = true whitelist = false authType = "BUNGEE" proxySecret = "default" diff --git a/defaultRunData/server-1/shutdown-config.toml b/defaultRunData/server-1/shutdown-config.toml index e69de29bb..21358535f 100644 --- a/defaultRunData/server-1/shutdown-config.toml +++ b/defaultRunData/server-1/shutdown-config.toml @@ -0,0 +1,2 @@ +shutdownMessage = "
<#ff2655>⚠ [WARNING] <#bf1f41>⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» The server needs to restart!
You can finish your current game, but you will be unable to join new ones.
<#bf1f41>⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" +forceShutdownMessage = "
<#ff2655>⚠ [WARNING] <#bf1f41>⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» The server will force restart in 5 minutes!
<#bf1f41>⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" diff --git a/defaultRunData/server-1/whisper-config.toml b/defaultRunData/server-1/whisper-config.toml new file mode 100644 index 000000000..1268d15ee --- /dev/null +++ b/defaultRunData/server-1/whisper-config.toml @@ -0,0 +1,5 @@ +toTargetFormat="<#d277c1>From <#b8bfde>> <#9096ad>" +toSenderFormat="<#d277c1>To <#b8bfde>> <#9096ad>" +fallbackNameColor = { r = 152, g = 255, b = 152 } +consoleName="<#fffa9e>✦ Console" +defaultName="Unknown" diff --git a/defaultRunData/server-1/whisper.toml b/defaultRunData/server-1/whisper.toml deleted file mode 100644 index b0362162e..000000000 --- a/defaultRunData/server-1/whisper.toml +++ /dev/null @@ -1,5 +0,0 @@ -toTargetFormat = "<#d277c1>From <#b8bfde>> <#9096ad>" -toSenderFormat = "<#d277c1>To <#b8bfde>> <#9096ad>" -fallbackNameColor = { r = 152, g = 255, b = 152 } -consoleName = "Console" -defaultName = "Unknown" diff --git a/defaultRunData/server-1/zombies-config.toml b/defaultRunData/server-1/zombies-config.toml new file mode 100644 index 000000000..9f5d04948 --- /dev/null +++ b/defaultRunData/server-1/zombies-config.toml @@ -0,0 +1,22 @@ +maximumScenes = 20 + +[gamereportConfig] +idleStageFormat = "<#9096ad>Idle" +countdownStageFormat = "Countdown" +inGameFormat = "Round <#b8bfde>- " +endedFormat = "Ended <#b8bfde>( - |1#WON - '>)" +gameEntryFormat = " /

<#b8bfde>: [WARP]" +previousPageFormat = " «" +nextPageFormat = "»" +pageFormat = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad><#b8bfde>/ Gamereport at UTC

<#b8bfde> Active Games


<#9096ad>Page
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" + +#⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
<#fffa9e>» <#9096ad><#b8bfde>/ Gamereport at UTC

<#b8bfde> Active Games



⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +# +# /

<#b8bfde>: [WARP] +# +# « <#9096ad>Page »⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +# +#<#9096ad>Idle +#Countdown +#Round <#b8bfde>- +#Ended <#b8bfde>( - |1#WON - '>) diff --git a/gradle.properties b/gradle.properties index 5cacfe897..d7fa8faaf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ # Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx2G diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83d2cd328..0243487c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,13 +3,15 @@ metadata.format.version = "1.1" [versions] adventure = "4.12.0" -ethylene = "0.20.1" +ethylene = "0.21.0" examination = "1.3.0" +hephaistos = "2.5.3" + junit-jupiter = "5.9.0" -minestom = "d9cd6dd44e" +minestom = "b8f34679c3" toolkit = "0.3.0" @@ -19,15 +21,18 @@ adventure-api = { group = "net.kyori", name = "adventure-api", version.ref = "ad adventure-key = { group = "net.kyori", name = "adventure-key", version.ref = "adventure" } adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } +adventure-text-serializer-gson = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" } examination-api = { module = "net.kyori:examination-api", version.ref = "examination" } examination-string = { module = "net.kyori:examination-string", version.ref = "examination" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.1.1" } +cloth-config = { module = "me.shedaniel.cloth:cloth-config-fabric", version = "11.1.106" } + commons-lang3 = { module = "org.apache.commons:commons-lang3", version = "3.12.0" } -element-core = { module = "com.github.steanky:element-core", version = "0.14.3" } +element-core = { module = "com.github.steanky:element-core", version = "0.15.2" } ethylene-core = { module = "com.github.steanky:ethylene-core", version.ref = "ethylene" } ethylene-hjson = { module = "com.github.steanky:ethylene-hjson", version.ref = "ethylene" } @@ -37,10 +42,14 @@ ethylene-yaml = { module = "com.github.steanky:ethylene-yaml", version.ref = "et ethylene-mapper = { module = "com.github.steanky:ethylene-mapper", version.ref = "ethylene" } fabric-loader = { module = "net.fabricmc:fabric-loader", version = "0.14.21" } -fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version = "0.83.0+1.19.4" } +fabric-api-oneNineteen = { module = "net.fabricmc.fabric-api:fabric-api", version = "0.83.0+1.19.4" } +fabric-api-oneTwenty = { module = "net.fabricmc.fabric-api:fabric-api", version = "0.85.0+1.20.1" } fastutil = { module = "it.unimi.dsi:fastutil", version = "8.5.8" } +hephaistos-common = { module = "io.github.jglrxavpok.hephaistos:common", version.ref = "hephaistos" } +hephaistos-gson = { module = "io.github.jglrxavpok.hephaistos:gson", version.ref = "hephaistos" } + hikariCP = { module = "com.zaxxer:HikariCP", version = "5.0.1" } jetbrains-annotations = { module = "org.jetbrains:annotations", version = "23.0.0" } @@ -54,7 +63,9 @@ libgui = { module = "io.github.cottonmc:LibGui", version = "7.1.0+1.19.4" } mariadb = { module = "org.mariadb.jdbc:mariadb-java-client", version = "3.0.10" } -minecraft = { module = "com.mojang:minecraft", version = "1.19.4" } +minecraft-oneNineteen = { module = "com.mojang:minecraft", version = "1.19.4" } + +minecraft-oneTwenty = { module = "com.mojang:minecraft", version = "1.20.1" } minestom = { module = "org.phantazm.Minestom:Minestom", version.ref = "minestom" } @@ -62,7 +73,9 @@ minestom-testing = { module = "org.phantazm.Minestom:testing", version.ref = "mi mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version = "4.7.0" } -proxima-core = { module = "com.github.steanky:proxima-core", version = "0.6.1" } +modmenu = { module = "com.terraformersmc:modmenu", version = "7.1.0" } + +proxima-core = { module = "com.github.steanky:proxima-core", version = "0.7.0" } renderer = { module = "com.github.0x3C50:Renderer", version = "e1bb03f45e" } @@ -77,7 +90,9 @@ vector-core = { module = "com.github.steanky:vector-core", version = "0.9.2" } velocity-api = { module = "com.velocitypowered:velocity-api", version = "3.1.2-SNAPSHOT" } -yarn-mappings = { module = "net.fabricmc:yarn", version = "1.19.4+build.2" } +yarn-mappings-oneNineteen = { module = "net.fabricmc:yarn", version = "1.19.4+build.2" } + +yarn-mappings-oneTwenty = { module = "net.fabricmc:yarn", version = "1.20.1+build.9" } [plugins] fabric-loom = { id = "fabric-loom", version = "1.1.9" } diff --git a/messaging/build.gradle.kts b/messaging/build.gradle.kts index 58f6acbb1..2aad065d7 100644 --- a/messaging/build.gradle.kts +++ b/messaging/build.gradle.kts @@ -1,3 +1,8 @@ plugins { id("phantazm.java-library-conventions") } + +dependencies { + implementation(projects.phantazmCommons) + api(libs.adventure.key) +} diff --git a/messaging/src/main/java/org/phantazm/messaging/MessageChannels.java b/messaging/src/main/java/org/phantazm/messaging/MessageChannels.java deleted file mode 100644 index 68304566e..000000000 --- a/messaging/src/main/java/org/phantazm/messaging/MessageChannels.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.phantazm.messaging; - -/** - * Contains shared message channel ID values. - * Message channels should use the 1.13+ namespaced key format. These constants represent the key's value. - */ -public final class MessageChannels { - - - /** - * The message channel between the proxy and a server. - */ - public static final String PROXY_TO_SERVER = "proxy2server"; - - /** - * The message channel between the client and a server. - */ - public static final String CLIENT_TO_SERVER = "client2server"; - - public static final String CLIENT_TO_PROXY = "client2proxy"; - - private MessageChannels() { - throw new UnsupportedOperationException(); - } - -} diff --git a/messaging/src/main/java/org/phantazm/messaging/packet/Packet.java b/messaging/src/main/java/org/phantazm/messaging/packet/Packet.java index 510116eac..cdeaa3ebd 100644 --- a/messaging/src/main/java/org/phantazm/messaging/packet/Packet.java +++ b/messaging/src/main/java/org/phantazm/messaging/packet/Packet.java @@ -1,5 +1,6 @@ package org.phantazm.messaging.packet; +import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.messaging.serialization.DataWriter; @@ -8,14 +9,7 @@ */ public interface Packet { - /** - * Gets the ID of the packet. This should be unique for a single messaging channel. - * For example, all the packets in the proxy plugin messaging channel should be unique. - * However, they can share IDs with packets on other channels. - * - * @return The ID of the packet - */ - byte getId(); + @NotNull Key getId(); /** * Writes the packet to a {@link DataWriter}. diff --git a/messaging/src/main/java/org/phantazm/messaging/packet/PacketHandler.java b/messaging/src/main/java/org/phantazm/messaging/packet/PacketHandler.java deleted file mode 100644 index a1481c92d..000000000 --- a/messaging/src/main/java/org/phantazm/messaging/packet/PacketHandler.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.phantazm.messaging.packet; - -import org.jetbrains.annotations.NotNull; -import org.phantazm.messaging.serialization.PacketSerializer; - -import java.util.Objects; - -/** - * A wrapper around a {@link PacketSerializer} that simplifies binary data reading and writing. - * - * @param The type of output that packets will be sent to - */ -public abstract class PacketHandler { - - private final PacketSerializer packetSerializer; - - public PacketHandler(@NotNull PacketSerializer packetSerializer) { - this.packetSerializer = Objects.requireNonNull(packetSerializer, "packetSerializer"); - } - - /** - * Handles incoming binary data. - * - * @param outputReceiver The output receiver for any packets that will be sent as a result of receiving this data - * @param data The data that was sent - */ - public void handleData(@NotNull TOutputReceiver outputReceiver, byte @NotNull [] data) { - packetSerializer.deserializePacket(data).ifPresent(packet -> handlePacket(outputReceiver, packet)); - } - - /** - * Handles an incoming packet. - * - * @param outputReceiver The output receiver for any packets that will be sent as a result of receiving this packet - * @param packet The packet that was sent - */ - protected abstract void handlePacket(@NotNull TOutputReceiver outputReceiver, @NotNull Packet packet); - - /** - * Sends binary data to a receiver. - * - * @param outputReceiver The binary data receiver - * @param data The binary data to send - */ - protected abstract void sendToReceiver(@NotNull TOutputReceiver outputReceiver, byte @NotNull [] data); - - /** - * Sends an outgoing packet to the output receiver. - * - * @param outputReceiver The output receiver - * @param packet The packet to send - */ - protected void output(@NotNull TOutputReceiver outputReceiver, @NotNull Packet packet) { - sendToReceiver(outputReceiver, packetSerializer.serializePacket(packet)); - } - -} diff --git a/messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionQueryPacket.java b/messaging/src/main/java/org/phantazm/messaging/packet/player/MapDataVersionQueryPacket.java similarity index 73% rename from messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionQueryPacket.java rename to messaging/src/main/java/org/phantazm/messaging/packet/player/MapDataVersionQueryPacket.java index 09d6e5568..0ed19070f 100644 --- a/messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionQueryPacket.java +++ b/messaging/src/main/java/org/phantazm/messaging/packet/player/MapDataVersionQueryPacket.java @@ -1,6 +1,8 @@ -package org.phantazm.messaging.packet.c2p; +package org.phantazm.messaging.packet.player; +import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Namespaces; import org.phantazm.messaging.packet.Packet; import org.phantazm.messaging.serialization.DataReader; import org.phantazm.messaging.serialization.DataWriter; @@ -12,10 +14,7 @@ */ public record MapDataVersionQueryPacket() implements Packet { - /** - * The ID of the {@link MapDataVersionQueryPacket}. - */ - public static final byte ID = 0; + public static final Key ID = Key.key(Namespaces.PHANTAZM, "client/mapdata_version_query"); public static @NotNull MapDataVersionQueryPacket read(@NotNull DataReader reader) { Objects.requireNonNull(reader, "reader"); @@ -23,7 +22,7 @@ public record MapDataVersionQueryPacket() implements Packet { } @Override - public byte getId() { + public @NotNull Key getId() { return ID; } diff --git a/messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionResponsePacket.java b/messaging/src/main/java/org/phantazm/messaging/packet/proxy/MapDataVersionResponsePacket.java similarity index 76% rename from messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionResponsePacket.java rename to messaging/src/main/java/org/phantazm/messaging/packet/proxy/MapDataVersionResponsePacket.java index 3df95f373..4f518c26e 100644 --- a/messaging/src/main/java/org/phantazm/messaging/packet/c2p/MapDataVersionResponsePacket.java +++ b/messaging/src/main/java/org/phantazm/messaging/packet/proxy/MapDataVersionResponsePacket.java @@ -1,6 +1,8 @@ -package org.phantazm.messaging.packet.c2p; +package org.phantazm.messaging.packet.proxy; +import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Namespaces; import org.phantazm.messaging.packet.Packet; import org.phantazm.messaging.serialization.DataReader; import org.phantazm.messaging.serialization.DataWriter; @@ -14,10 +16,7 @@ */ public record MapDataVersionResponsePacket(int version) implements Packet { - /** - * The ID of the {@link MapDataVersionResponsePacket}. - */ - public static final byte ID = 1; + public static final Key ID = Key.key(Namespaces.PHANTAZM, "proxy/mapdata_version_response"); public static @NotNull MapDataVersionResponsePacket read(@NotNull DataReader reader) { Objects.requireNonNull(reader, "reader"); @@ -25,7 +24,7 @@ public record MapDataVersionResponsePacket(int version) implements Packet { } @Override - public byte getId() { + public @NotNull Key getId() { return ID; } diff --git a/messaging/src/main/java/org/phantazm/messaging/packet/server/RoundStartPacket.java b/messaging/src/main/java/org/phantazm/messaging/packet/server/RoundStartPacket.java new file mode 100644 index 000000000..9fdcb2d06 --- /dev/null +++ b/messaging/src/main/java/org/phantazm/messaging/packet/server/RoundStartPacket.java @@ -0,0 +1,30 @@ +package org.phantazm.messaging.packet.server; + +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Namespaces; +import org.phantazm.messaging.packet.Packet; +import org.phantazm.messaging.serialization.DataReader; +import org.phantazm.messaging.serialization.DataWriter; + +import java.util.Objects; + +public record RoundStartPacket() implements Packet { + + public static final Key ID = Key.key(Namespaces.PHANTAZM, "server/round_start"); + + public static @NotNull RoundStartPacket read(@NotNull DataReader reader) { + Objects.requireNonNull(reader, "reader"); + return new RoundStartPacket(); + } + + @Override + public @NotNull Key getId() { + return ID; + } + + @Override + public void write(@NotNull DataWriter dataWriter) { + + } +} diff --git a/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializer.java b/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializer.java deleted file mode 100644 index 9b095f8d5..000000000 --- a/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializer.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.phantazm.messaging.serialization; - -import org.jetbrains.annotations.NotNull; -import org.phantazm.messaging.packet.Packet; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Converts packets from byte arrays to {@link Packet}s and vice versa. - */ -public class PacketSerializer { - - private final Map> packetDeserializerMap; - - private final Supplier writerCreator; - - private final Function readerCreator; - - /** - * Creates a {@link PacketSerializer}. - * - * @param packetDeserializerMap A {@link Map} of byte packet IDs to {@link Packet} creators from a {@link DataReader} - * @param writerCreator A creator for serialization {@link DataWriter}s - * @param readerCreator A creator for deserialization {@link DataReader}s - */ - public PacketSerializer(@NotNull Map> packetDeserializerMap, - @NotNull Supplier writerCreator, @NotNull Function readerCreator) { - this.packetDeserializerMap = Objects.requireNonNull(packetDeserializerMap, "packetDeserializerMap"); - this.writerCreator = Objects.requireNonNull(writerCreator, "writerCreator"); - this.readerCreator = Objects.requireNonNull(readerCreator, "readerCreator"); - } - - /** - * Serializes a packet into a byte array. - * - * @param packet The packet to serialize - * @return The byte array representation of the packet - */ - public byte[] serializePacket(@NotNull Packet packet) { - DataWriter dataWriter = writerCreator.get(); - dataWriter.writeByte(packet.getId()); - packet.write(dataWriter); - - return dataWriter.toByteArray(); - } - - /** - * Deserializes a packet from a byte array. - * - * @param bytes The byte array representation of the packet - * @return An {@link Optional} of the deserialized packet which is empty if deserialization fails - */ - public @NotNull Optional deserializePacket(byte @NotNull [] bytes) { - DataReader dataReader = readerCreator.apply(bytes); - byte id = dataReader.readByte(); - - Function deserializer = packetDeserializerMap.get(id); - if (deserializer == null) { - return Optional.empty(); - } - - return Optional.of(deserializer.apply(dataReader)); - } - -} diff --git a/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializers.java b/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializers.java deleted file mode 100644 index bfd064984..000000000 --- a/messaging/src/main/java/org/phantazm/messaging/serialization/PacketSerializers.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.phantazm.messaging.serialization; - -import org.jetbrains.annotations.NotNull; -import org.phantazm.messaging.packet.Packet; -import org.phantazm.messaging.packet.c2p.MapDataVersionQueryPacket; -import org.phantazm.messaging.packet.c2p.MapDataVersionResponsePacket; - -import java.util.Collections; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Creates common {@link PacketSerializer}s. - */ -public final class PacketSerializers { - - private static final Map> clientToProxyDeserializers; - - static { - clientToProxyDeserializers = - Map.of(MapDataVersionQueryPacket.ID, MapDataVersionQueryPacket::read, MapDataVersionResponsePacket.ID, - MapDataVersionResponsePacket::read); - } - - private PacketSerializers() { - throw new UnsupportedOperationException(); - } - - public static @NotNull PacketSerializer clientToServerSerializer(@NotNull Supplier writerCreator, - @NotNull Function readerCreator) { - return new PacketSerializer(Collections.emptyMap(), writerCreator, readerCreator); - } - - public static @NotNull PacketSerializer clientToProxySerializer(@NotNull Supplier writerCreator, - @NotNull Function readerCreator) { - return new PacketSerializer(clientToProxyDeserializers, writerCreator, readerCreator); - } - -} diff --git a/minestom b/minestom index 05a7ad75b..5916290c0 160000 --- a/minestom +++ b/minestom @@ -1 +1 @@ -Subproject commit 05a7ad75b4e0879bcba9c49afdd7ae0457cb67d3 +Subproject commit 5916290c0ce29abd070c121791e6518569f9158a diff --git a/mob/src/main/java/org/phantazm/mob/MobStore.java b/mob/src/main/java/org/phantazm/mob/MobStore.java index dcef537fc..42d800b48 100644 --- a/mob/src/main/java/org/phantazm/mob/MobStore.java +++ b/mob/src/main/java/org/phantazm/mob/MobStore.java @@ -8,6 +8,8 @@ import org.phantazm.commons.Namespaces; import org.phantazm.commons.Tickable; import org.phantazm.mob.skill.Skill; +import org.phantazm.proxima.bindings.minestom.goal.GoalGroup; +import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -49,6 +51,7 @@ public void useTrigger(@NotNull Entity entity, @NotNull Key key) { public void onMobDeath(@NotNull EntityDeathEvent event) { UUID uuid = event.getEntity().getUuid(); + tickableSkills.remove(uuid); PhantazmMob mob = uuidToMob.remove(uuid); if (mob != null) { Collection deathSkills = mob.triggers().get(DEATH_KEY); @@ -57,9 +60,23 @@ public void onMobDeath(@NotNull EntityDeathEvent event) { skill.use(mob); } } - } - tickableSkills.remove(uuid); + for (Collection skills : mob.triggers().values()) { + for (Skill skill : skills) { + skill.end(mob); + } + } + + for (GoalGroup group : mob.entity().goalGroups()) { + for (ProximaGoal goal : group.goals()) { + if (goal instanceof SkillDelegatingGoal skillDelegatingGoal) { + for (Skill skill : skillDelegatingGoal.skills()) { + skill.end(mob); + } + } + } + } + } } /** diff --git a/mob/src/main/java/org/phantazm/mob/SkillDelegatingGoal.java b/mob/src/main/java/org/phantazm/mob/SkillDelegatingGoal.java new file mode 100644 index 000000000..d7f6a293a --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/SkillDelegatingGoal.java @@ -0,0 +1,12 @@ +package org.phantazm.mob; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.phantazm.mob.skill.Skill; +import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; + +import java.util.Collection; + +public interface SkillDelegatingGoal extends ProximaGoal { + @NotNull @Unmodifiable Collection skills(); +} diff --git a/mob/src/main/java/org/phantazm/mob/config/MobModelConfigProcessor.java b/mob/src/main/java/org/phantazm/mob/config/MobModelConfigProcessor.java index 583c6824f..5637e20c1 100644 --- a/mob/src/main/java/org/phantazm/mob/config/MobModelConfigProcessor.java +++ b/mob/src/main/java/org/phantazm/mob/config/MobModelConfigProcessor.java @@ -80,7 +80,7 @@ public MobModelConfigProcessor(@NotNull ContextManager contextManager, ElementContext context = contextManager.makeContext(element.getNodeOrThrow("pathfindingSettings")); Pathfinding.Factory factory = context.provide(HANDLER, - () -> new GroundPathfindingFactory(new GroundPathfindingFactory.Data(1, 4, 0.5F))); + () -> new GroundPathfindingFactory(new GroundPathfindingFactory.Data(1, 4, 0.5F, 0, false))); ConfigNode metaNode = element.getNodeOrDefault(ConfigNode::of, "meta"); ConfigNode extraNode = element.getNodeOrDefault(ConfigNode::of, "extra"); diff --git a/mob/src/main/java/org/phantazm/mob/goal/CollectionGoalGroup.java b/mob/src/main/java/org/phantazm/mob/goal/CollectionGoalGroup.java index 3c116b493..cc54653b7 100644 --- a/mob/src/main/java/org/phantazm/mob/goal/CollectionGoalGroup.java +++ b/mob/src/main/java/org/phantazm/mob/goal/CollectionGoalGroup.java @@ -1,10 +1,13 @@ package org.phantazm.mob.goal; +import com.github.steanky.toolkit.collection.Containers; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import org.phantazm.proxima.bindings.minestom.goal.GoalGroup; import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.Optional; @@ -54,4 +57,9 @@ public void tick(long time) { public @NotNull Optional currentGoal() { return Optional.ofNullable(activeGoal); } + + @Override + public @NotNull @Unmodifiable List goals() { + return Containers.arrayView(goals); + } } diff --git a/mob/src/main/java/org/phantazm/mob/goal/MeleeAttackGoal.java b/mob/src/main/java/org/phantazm/mob/goal/MeleeAttackGoal.java index d74e517b7..f9a80c32a 100644 --- a/mob/src/main/java/org/phantazm/mob/goal/MeleeAttackGoal.java +++ b/mob/src/main/java/org/phantazm/mob/goal/MeleeAttackGoal.java @@ -1,20 +1,28 @@ package org.phantazm.mob.goal; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import net.minestom.server.MinecraftServer; import net.minestom.server.attribute.Attribute; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; -import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.entity.damage.Damage; +import net.minestom.server.timer.ExecutionType; +import net.minestom.server.timer.TaskSchedule; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.phantazm.core.Tags; import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.SkillDelegatingGoal; import org.phantazm.mob.skill.Skill; -import org.phantazm.mob.target.LastHitSelector; import org.phantazm.proxima.bindings.minestom.ProximaEntity; import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; import java.util.Collection; +import java.util.Collections; import java.util.Objects; @Model("mob.goal.melee_attack") @@ -22,48 +30,66 @@ public class MeleeAttackGoal implements GoalCreator { private final Data data; private final Collection skills; - private final LastHitSelector lastHitSelector; @FactoryMethod - public MeleeAttackGoal(@NotNull Data data, @NotNull @Child("skills") Collection skills, - @NotNull @Child("last_hit_selector") LastHitSelector lastHitSelector) { + public MeleeAttackGoal(@NotNull Data data, @NotNull @Child("skills") Collection skills) { this.data = Objects.requireNonNull(data, "data"); this.skills = Objects.requireNonNull(skills, "skills"); - this.lastHitSelector = Objects.requireNonNull(lastHitSelector, "lastHitSelector"); } @Override public @NotNull ProximaGoal create(@NotNull PhantazmMob mob) { - return new Goal(data, skills, lastHitSelector, mob); + return new Goal(data, skills, mob); } - private static class Goal implements ProximaGoal { + private static class Goal implements SkillDelegatingGoal { private final Data data; private final Collection skills; - private final LastHitSelector lastHitSelector; private final PhantazmMob mob; private long lastAttackTime; - @FactoryMethod - public Goal(@NotNull Data data, @NotNull Collection skills, - @NotNull LastHitSelector lastHitSelector, @NotNull PhantazmMob mob) { + public Goal(@NotNull Data data, @NotNull Collection skills, @NotNull PhantazmMob mob) { this.data = Objects.requireNonNull(data, "data"); this.skills = Objects.requireNonNull(skills, "skills"); - this.lastHitSelector = Objects.requireNonNull(lastHitSelector, "lastHitSelector"); this.mob = Objects.requireNonNull(mob, "mob"); + + Skill[] tickableSkills = skills.stream().filter(Skill::needsTicking).toArray(Skill[]::new); + if (tickableSkills.length == 0) { + return; + } + + mob.entity().scheduler().scheduleTask(() -> { + if (mob.entity().isDead() || mob.entity().isRemoved()) { + return; + } + + long time = System.currentTimeMillis(); + for (Skill skill : tickableSkills) { + skill.tick(time, mob); + } + }, TaskSchedule.immediate(), TaskSchedule.nextTick(), ExecutionType.SYNC); } @Override public boolean shouldStart() { ProximaEntity self = mob.entity(); - if ((System.currentTimeMillis() - lastAttackTime) / MinecraftServer.TICK_MS >= data.cooldown()) { + + Attribute attribute = Attribute.fromKey("phantazm.attack_speed_multiplier"); + float attackSpeedMultiplier = 1F; + if (attribute != null) { + attackSpeedMultiplier = self.getAttributeValue(attribute); + } + + if ((float)((System.currentTimeMillis() - lastAttackTime) / MinecraftServer.TICK_MS) * + attackSpeedMultiplier >= data.cooldown()) { Entity target = self.getTargetEntity(); if (target == null) { return false; } - return self.getDistanceSquared(target) <= data.range * data.range; + return self.getBoundingBox().expand(data.range, data.range, data.range) + .intersectEntity(self.getPosition(), target); } return false; @@ -85,13 +111,14 @@ public void start() { float knockbackStrength = self.getAttributeValue(Attribute.ATTACK_KNOCKBACK); double angle = pos.yaw() * (Math.PI / 180); - boolean damaged = livingEntity.damage(DamageType.fromEntity(self), damageAmount, data.bypassArmor); + boolean damaged = livingEntity.damage(Damage.fromEntity(self, damageAmount), data.bypassArmor); - if (damaged) { - livingEntity.takeKnockback(0.4F * knockbackStrength, Math.sin(angle), -Math.cos(angle)); + if (!damaged) { + return; } - lastHitSelector.setLastHit(livingEntity); + livingEntity.takeKnockback(knockbackStrength, data.horizontal, Math.sin(angle), -Math.cos(angle)); + self.setTag(Tags.LAST_MELEE_HIT_TAG, livingEntity.getUuid()); for (Skill skill : skills) { skill.use(mob); @@ -105,6 +132,11 @@ public void start() { public boolean shouldEnd() { return true; } + + @Override + public @NotNull @Unmodifiable Collection skills() { + return Collections.unmodifiableCollection(skills); + } } @DataObject @@ -112,12 +144,21 @@ public record Data(long cooldown, double range, boolean swingHand, boolean bypassArmor, - @NotNull @ChildPath("skills") Collection skillPaths, - @NotNull @ChildPath("last_hit_selector") String lastHitSelectorPath) { + boolean horizontal, + @NotNull @ChildPath("skills") Collection skillPaths) { + @Default("swingHand") + public static @NotNull ConfigElement defaultSwingHand() { + return ConfigPrimitive.of(true); + } - public Data { - Objects.requireNonNull(lastHitSelectorPath, "lastHitSelectorPath"); + @Default("bypassArmor") + public static @NotNull ConfigElement defaultBypassArmor() { + return ConfigPrimitive.of(false); } + @Default("horizontal") + public static @NotNull ConfigElement defaultHorizontal() { + return ConfigPrimitive.of(false); + } } } diff --git a/mob/src/main/java/org/phantazm/mob/goal/PlayStepSoundGoal.java b/mob/src/main/java/org/phantazm/mob/goal/PlayStepSoundGoal.java index 401e33e32..e24fc054a 100644 --- a/mob/src/main/java/org/phantazm/mob/goal/PlayStepSoundGoal.java +++ b/mob/src/main/java/org/phantazm/mob/goal/PlayStepSoundGoal.java @@ -1,5 +1,6 @@ package org.phantazm.mob.goal; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.sound.Sound; @@ -21,6 +22,7 @@ // TODO: this naively tracks movement, whereas vanilla MC is handling velocity changes. Find a better way to do this @Model("mob.goal.play_step_sound") +@Cache(false) public class PlayStepSoundGoal implements GoalCreator { private final Random random; diff --git a/mob/src/main/java/org/phantazm/mob/goal/ProjectileMovementGoal.java b/mob/src/main/java/org/phantazm/mob/goal/ProjectileMovementGoal.java index 32de51707..07cfa8392 100644 --- a/mob/src/main/java/org/phantazm/mob/goal/ProjectileMovementGoal.java +++ b/mob/src/main/java/org/phantazm/mob/goal/ProjectileMovementGoal.java @@ -14,6 +14,7 @@ import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent; import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent; import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; @@ -21,14 +22,10 @@ import java.util.Collection; import java.util.Objects; -import java.util.Optional; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; -import java.util.stream.Stream; public class ProjectileMovementGoal implements ProximaGoal { - private static final int COLLISION_TICK_THRESHOLD = 3; private static final double MAGIC = 0.007499999832361937D; //what is this? nobody knows... private final Entity entity; @@ -45,6 +42,9 @@ public class ProjectileMovementGoal implements ProximaGoal { private boolean collided = false; + private final int selfCollisionTickThreshold; + private final int blockCollisionTickThreshold; + public ProjectileMovementGoal(@NotNull Entity entity, @NotNull Entity shooter, @NotNull Point to, double power, double spread) { this.entity = Objects.requireNonNull(entity, "entity"); @@ -52,6 +52,14 @@ public ProjectileMovementGoal(@NotNull Entity entity, @NotNull Entity shooter, @ this.to = Objects.requireNonNull(to, "to"); this.power = power; this.spread = spread; + + //power is in blocks/s + this.selfCollisionTickThreshold = (int)Math.ceil( + ((entity.getBoundingBox().width() / 2) + (shooter.getBoundingBox().width() / 2) / power) * + MinecraftServer.TICK_PER_SECOND); + + this.blockCollisionTickThreshold = + (int)Math.ceil(((entity.getBoundingBox().width() / 2) / power) * MinecraftServer.TICK_PER_SECOND); } @Override @@ -105,12 +113,8 @@ public void start() { @Override public void tick(long time) { - if (previousPos == null) { - return; - } - Pos currentPos = entity.getPosition(); - if (checkStuck(previousPos, currentPos)) { + if (checkStuck(previousPos == null ? currentPos : previousPos, currentPos)) { if (collided) { return; } @@ -123,6 +127,8 @@ else if (collided) { entity.setNoGravity(false); EventDispatcher.call(new ProjectileUncollideEvent(entity)); } + + this.previousPos = currentPos; } @SuppressWarnings("UnstableApiUsage") @@ -131,17 +137,21 @@ private boolean checkStuck(Pos previousPos, Pos currentPos) { if (instance == null) { return false; } + + Chunk chunk = entity.getChunk(); + if (chunk == null) { + return false; + } + final long aliveTicks = entity.getAliveTicks(); if (previousPos.samePoint(currentPos)) { Block block = instance.getBlock(previousPos); - return block.isSolid() && !block.compare(Block.BARRIER) && aliveTicks >= COLLISION_TICK_THRESHOLD && + return block.isSolid() && !block.compare(Block.BARRIER) && aliveTicks >= blockCollisionTickThreshold && block.registry().collisionShape().intersectBox( previousPos.sub(previousPos.blockX(), previousPos.blockY(), previousPos.blockZ()), entity.getBoundingBox()); } - Chunk chunk = null; - Collection entities = null; final BoundingBox bb = entity.getBoundingBox(); final double part = bb.width() / 2; @@ -158,7 +168,7 @@ private boolean checkStuck(Pos previousPos, Pos currentPos) { block = instance.getBlock(previousPos); blockPos = previousPos; } - if (block.isSolid() && !block.compare(Block.BARRIER) && aliveTicks >= COLLISION_TICK_THRESHOLD && + if (block.isSolid() && !block.compare(Block.BARRIER) && aliveTicks >= blockCollisionTickThreshold && block.registry().collisionShape() .intersectBox(previousPos.sub(blockPos.blockX(), blockPos.blockY(), blockPos.blockZ()), entity.getBoundingBox())) { @@ -171,26 +181,18 @@ private boolean checkStuck(Pos previousPos, Pos currentPos) { return true; } } - if (entity.getChunk() != chunk) { - chunk = entity.getChunk(); - entities = instance.getChunkEntities(chunk).stream().filter(entity -> entity instanceof LivingEntity) - .map(entity -> (LivingEntity)entity).collect(Collectors.toSet()); - } - if (entities == null) { - return false; // should never happen - } - Point finalPreviousPos = previousPos; - Stream victimsStream = entities.stream() - .filter(entity -> bb.intersectEntity(finalPreviousPos, entity) && entity != this.entity); - if (aliveTicks < COLLISION_TICK_THRESHOLD) { - victimsStream = victimsStream.filter(entity -> entity != shooter); - } - final Optional victimOptional = victimsStream.findAny(); - if (victimOptional.isPresent()) { - final LivingEntity target = victimOptional.get(); + Collection entities = instance.getEntityTracker() + .chunkEntities(chunk.getChunkX(), chunk.getChunkZ(), EntityTracker.Target.LIVING_ENTITIES); + + for (Entity victim : entities) { + if (victim == this.entity || (victim == shooter && aliveTicks < selfCollisionTickThreshold) || + !bb.intersectEntity(previousPos, victim)) { + continue; + } + final ProjectileCollideWithEntityEvent event = - new ProjectileCollideWithEntityEvent(entity, previousPos, target); + new ProjectileCollideWithEntityEvent(entity, previousPos, victim); EventDispatcher.call(event); if (!event.isCancelled()) { return collided || entity.isOnGround(); diff --git a/mob/src/main/java/org/phantazm/mob/goal/UseSkillGoal.java b/mob/src/main/java/org/phantazm/mob/goal/UseSkillGoal.java deleted file mode 100644 index 65a8881e8..000000000 --- a/mob/src/main/java/org/phantazm/mob/goal/UseSkillGoal.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.phantazm.mob.goal; - -import com.github.steanky.element.core.annotation.*; -import net.minestom.server.MinecraftServer; -import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.PhantazmMob; -import org.phantazm.mob.skill.Skill; -import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; - -import java.util.Objects; - -/** - * A {@link ProximaGoal} that periodically uses a {@link Skill}. - */ -@Model("mob.goal.use_skill") -@Cache(false) -public class UseSkillGoal implements GoalCreator { - private final Data data; - private final Skill skill; - - @FactoryMethod - public UseSkillGoal(@NotNull Data data, @NotNull @Child("skill") Skill skill) { - this.data = Objects.requireNonNull(data, "data"); - this.skill = Objects.requireNonNull(skill, "skill"); - } - - @Override - public @NotNull ProximaGoal create(@NotNull PhantazmMob mob) { - return new Goal(data, skill, mob); - } - - - private static class Goal implements ProximaGoal { - private final Data data; - private final Skill skill; - private final PhantazmMob self; - - private long lastUsage; - - public Goal(@NotNull Data data, @NotNull Skill skill, @NotNull PhantazmMob self) { - this.data = Objects.requireNonNull(data, "data"); - this.skill = Objects.requireNonNull(skill, "skill"); - this.self = Objects.requireNonNull(self, "self"); - this.lastUsage = -1; - } - - @Override - public boolean shouldStart() { - return true; - } - - @Override - public boolean shouldEnd() { - return false; - } - - @Override - public void start() { - - } - - @Override - public void tick(long time) { - if (lastUsage == -1) { - lastUsage = time; - return; - } - - if ((time - lastUsage) / MinecraftServer.TICK_MS >= data.period()) { - skill.use(self); - lastUsage = time; - } - } - - @Override - public void end() { - - } - } - - @DataObject - public record Data(@NotNull @ChildPath("skill") String skillPath, long period) { - - public Data { - Objects.requireNonNull(skillPath, "skillPath"); - } - } - -} diff --git a/mob/src/main/java/org/phantazm/mob/skill/AddVelocitySkill.java b/mob/src/main/java/org/phantazm/mob/skill/AddVelocitySkill.java new file mode 100644 index 000000000..6101b14ac --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/skill/AddVelocitySkill.java @@ -0,0 +1,52 @@ +package org.phantazm.mob.skill; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.vector.Vec3D; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.potion.Potion; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.VecUtils; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.target.TargetSelector; + +import java.util.Objects; + +@Model("mob.skill.add_velocity") +@Cache(false) +public class AddVelocitySkill implements Skill { + + private final Data data; + private final TargetSelector selector; + + @FactoryMethod + public AddVelocitySkill(@NotNull Data data, @NotNull @Child("selector") TargetSelector selector) { + this.data = Objects.requireNonNull(data, "data"); + this.selector = Objects.requireNonNull(selector, "selector"); + } + + @Override + public void use(@NotNull PhantazmMob self) { + selector.selectTarget(self).ifPresent(livingEntity -> { + if (livingEntity instanceof Iterable iterable) { + Iterable entityIterable = (Iterable)iterable; + for (LivingEntity entity : entityIterable) { + entity.setVelocity(entity.getVelocity().add(VecUtils.toPoint(data.delta()))); + } + } + else if (livingEntity instanceof LivingEntity living) { + living.setVelocity(living.getVelocity().add(VecUtils.toPoint(data.delta()))); + } + }); + } + + @DataObject + public record Data(@NotNull @ChildPath("selector") String selector, @NotNull Vec3D delta) { + + public Data { + Objects.requireNonNull(selector, "selector"); + } + + } + +} diff --git a/mob/src/main/java/org/phantazm/mob/skill/ApplyPotionSkill.java b/mob/src/main/java/org/phantazm/mob/skill/ApplyPotionSkill.java new file mode 100644 index 000000000..ae5ff6c0e --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/skill/ApplyPotionSkill.java @@ -0,0 +1,49 @@ +package org.phantazm.mob.skill; + +import com.github.steanky.element.core.annotation.*; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.potion.Potion; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.target.TargetSelector; + +import java.util.Objects; + +@Model("mob.skill.potion") +@Cache(false) +public class ApplyPotionSkill implements Skill { + + private final Data data; + private final TargetSelector selector; + + @FactoryMethod + public ApplyPotionSkill(@NotNull Data data, @NotNull @Child("selector") TargetSelector selector) { + this.data = Objects.requireNonNull(data, "data"); + this.selector = Objects.requireNonNull(selector, "selector"); + } + + @Override + public void use(@NotNull PhantazmMob self) { + selector.selectTarget(self).ifPresent(livingEntity -> { + if (livingEntity instanceof Iterable iterable) { + Iterable entityIterable = (Iterable)iterable; + for (LivingEntity entity : entityIterable) { + entity.addEffect(data.potion()); + } + } + else if (livingEntity instanceof LivingEntity living) { + living.addEffect(data.potion()); + } + }); + } + + @DataObject + public record Data(@NotNull @ChildPath("selector") String selector, @NotNull Potion potion) { + + public Data { + Objects.requireNonNull(selector, "selector"); + } + + } + +} diff --git a/mob/src/main/java/org/phantazm/mob/skill/AttributeModifyingSkill.java b/mob/src/main/java/org/phantazm/mob/skill/AttributeModifyingSkill.java deleted file mode 100644 index 9a2d001b4..000000000 --- a/mob/src/main/java/org/phantazm/mob/skill/AttributeModifyingSkill.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.phantazm.mob.skill; - -import com.github.steanky.element.core.annotation.Cache; -import com.github.steanky.element.core.annotation.DataObject; -import com.github.steanky.element.core.annotation.FactoryMethod; -import com.github.steanky.element.core.annotation.Model; -import net.minestom.server.attribute.Attribute; -import net.minestom.server.attribute.AttributeModifier; -import net.minestom.server.attribute.AttributeOperation; -import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.PhantazmMob; - -import java.util.UUID; - -@Model("mob.skill.attribute_modifying") -@Cache(false) -public class AttributeModifyingSkill implements Skill { - private final Data data; - private final UUID uuid; - private final String uuidString; - private final Attribute attribute; - - @FactoryMethod - public AttributeModifyingSkill(@NotNull Data data) { - this.data = data; - this.uuid = UUID.randomUUID(); - this.uuidString = this.uuid.toString(); - this.attribute = Attribute.fromKey(data.attribute); - } - - @Override - public void use(@NotNull PhantazmMob self) { - if (attribute != null) { - self.entity().getAttribute(attribute) - .addModifier(new AttributeModifier(uuid, uuidString, data.amount, data.attributeOperation)); - } - - } - - @Override - public void end(@NotNull PhantazmMob self) { - if (attribute != null) { - self.entity().getAttribute(attribute).removeModifier(uuid); - } - } - - @DataObject - public record Data(@NotNull String attribute, float amount, @NotNull AttributeOperation attributeOperation) { - } -} diff --git a/mob/src/main/java/org/phantazm/mob/skill/BleedEntitiesSkill.java b/mob/src/main/java/org/phantazm/mob/skill/BleedEntitiesSkill.java index eb9e4b248..0bff020c2 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/BleedEntitiesSkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/BleedEntitiesSkill.java @@ -2,10 +2,11 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.LivingEntity; -import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.entity.damage.Damage; import org.jetbrains.annotations.NotNull; import org.phantazm.mob.PhantazmMob; import org.phantazm.mob.target.TargetSelector; +import org.phantazm.mob.validator.TargetValidator; import java.util.Collection; import java.util.Iterator; @@ -15,44 +16,61 @@ @Model("mob.skill.bleed") @Cache(false) public class BleedEntitiesSkill implements Skill { - private final Collection bleeding = new LinkedList<>(); private final Data data; - private final TargetSelector selector; + private final TargetSelector selector; + private final TargetValidator validator; @FactoryMethod - public BleedEntitiesSkill(@NotNull Data data, - @NotNull @Child("selector") TargetSelector selector) { + public BleedEntitiesSkill(@NotNull Data data, @NotNull @Child("selector") TargetSelector selector, + @NotNull @Child("validator") TargetValidator validator) { this.data = Objects.requireNonNull(data, "data"); this.selector = Objects.requireNonNull(selector, "selector"); + this.validator = Objects.requireNonNull(validator, "validator"); } @Override public void use(@NotNull PhantazmMob self) { - selector.selectTarget(self).ifPresent(livingEntity -> bleeding.add(new BleedContext(livingEntity, 0L))); + selector.selectTarget(self).ifPresent(target -> { + if (target instanceof LivingEntity livingEntity) { + bleeding.add(new BleedContext(self, livingEntity, 0L)); + } + else if (target instanceof Iterable iterable) { + for (Object object : iterable) { + if (object instanceof LivingEntity livingEntity) { + bleeding.add(new BleedContext(self, livingEntity, 0L)); + } + } + } + }); } @Override public void tick(long time, @NotNull PhantazmMob self) { Iterator contextIterator = bleeding.iterator(); - if (!contextIterator.hasNext()) { - return; - } + while (contextIterator.hasNext()) { + BleedContext bleedContext = contextIterator.next(); + if (bleedContext.self != self) { + continue; + } - for (BleedContext bleedContext = contextIterator.next(); contextIterator.hasNext(); - bleedContext = contextIterator.next()) { LivingEntity livingEntity = bleedContext.target(); if (livingEntity.isRemoved()) { contextIterator.remove(); continue; } + if (!validator.valid(self.entity(), livingEntity)) { + contextIterator.remove(); + continue; + } + long ticksSinceStart = bleedContext.ticksSinceStart(); - bleedContext.setTicksSinceStart(ticksSinceStart + 1); + bleedContext.incrementTicks(); if (ticksSinceStart % data.bleedInterval() == 0) { - livingEntity.damage(DamageType.fromEntity(self.entity()), data.bleedDamage(), data.bypassArmor); - contextIterator.remove(); + livingEntity.damage(Damage.fromEntity(self.entity(), data.bleedDamage()), data.bypassArmor); } + if (ticksSinceStart >= data.bleedTime()) { contextIterator.remove(); } @@ -66,21 +84,12 @@ public boolean needsTicking() { @Override public void end(@NotNull PhantazmMob self) { - Iterator contextIterator = bleeding.iterator(); - if (!contextIterator.hasNext()) { - return; - } - - while (contextIterator.hasNext()) { - BleedContext next = contextIterator.next(); - if (next.target == self.entity()) { - contextIterator.remove(); - } - } + bleeding.removeIf(next -> next.self == self); } @DataObject public record Data(@NotNull @ChildPath("selector") String selector, + @NotNull @ChildPath("validator") String validator, float bleedDamage, boolean bypassArmor, long bleedInterval, @@ -93,26 +102,27 @@ public record Data(@NotNull @ChildPath("selector") String selector, } private static final class BleedContext { - + private final PhantazmMob self; private final LivingEntity target; - private long ticksSinceStart; + private long ticksSinceStart; - public BleedContext(@NotNull LivingEntity target, long ticksSinceStart) { - this.target = Objects.requireNonNull(target, "target"); + private BleedContext(@NotNull PhantazmMob self, @NotNull LivingEntity target, long ticksSinceStart) { + this.self = self; + this.target = target; this.ticksSinceStart = ticksSinceStart; } - public @NotNull LivingEntity target() { + private LivingEntity target() { return target; } - public long ticksSinceStart() { + private long ticksSinceStart() { return ticksSinceStart; } - public void setTicksSinceStart(long ticksSinceStart) { - this.ticksSinceStart = ticksSinceStart; + private void incrementTicks() { + ticksSinceStart++; } } diff --git a/mob/src/main/java/org/phantazm/mob/skill/DamageEntitySkill.java b/mob/src/main/java/org/phantazm/mob/skill/DamageEntitySkill.java index 68446f0e8..878f422c4 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/DamageEntitySkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/DamageEntitySkill.java @@ -2,6 +2,7 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.entity.damage.DamageType; import org.jetbrains.annotations.NotNull; import org.phantazm.mob.PhantazmMob; @@ -29,11 +30,11 @@ public void use(@NotNull PhantazmMob self) { if (livingEntity instanceof Iterable iterable) { Iterable entityIterable = (Iterable)iterable; for (LivingEntity entity : entityIterable) { - entity.damage(DamageType.fromEntity(self.entity()), data.damage(), data.bypassArmor); + entity.damage(Damage.fromEntity(self.entity(), data.damage()), data.bypassArmor); } } else if (livingEntity instanceof LivingEntity living) { - living.damage(DamageType.fromEntity(self.entity()), data.damage(), data.bypassArmor); + living.damage(Damage.fromEntity(self.entity(), data.damage()), data.bypassArmor); } }); } diff --git a/mob/src/main/java/org/phantazm/mob/skill/PushEntitySkill.java b/mob/src/main/java/org/phantazm/mob/skill/PushEntitySkill.java new file mode 100644 index 000000000..441bef303 --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/skill/PushEntitySkill.java @@ -0,0 +1,49 @@ +package org.phantazm.mob.skill; + +import com.github.steanky.element.core.annotation.*; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.target.TargetSelector; + +import java.util.Objects; + +@Model("mob.skill.push") +@Cache(false) +public class PushEntitySkill implements Skill { + private final Data data; + private final TargetSelector selector; + + @FactoryMethod + public PushEntitySkill(@NotNull Data data, @NotNull @Child("selector") TargetSelector selector) { + this.data = Objects.requireNonNull(data, "data"); + this.selector = Objects.requireNonNull(selector, "selector"); + } + + @Override + public void use(@NotNull PhantazmMob self) { + selector.selectTarget(self).ifPresent(livingEntity -> { + if (livingEntity instanceof Iterable iterable) { + for (Object object : iterable) { + if (object instanceof Entity entity) { + setVelocity(entity, self.entity()); + } + } + } + else if (livingEntity instanceof LivingEntity living) { + setVelocity(living, self.entity()); + } + }); + } + + private void setVelocity(Entity target, Entity self) { + Vec diff = target.getPosition().sub(self.getPosition()).asVec().normalize(); + target.setVelocity(diff.mul(data.power).add(0, data.vertical, 0)); + } + + @DataObject + public record Data(@NotNull @ChildPath("selector") String selector, double power, double vertical) { + } +} diff --git a/mob/src/main/java/org/phantazm/mob/skill/RadialDamageEntitySkill.java b/mob/src/main/java/org/phantazm/mob/skill/RadialDamageEntitySkill.java index 3aae5f826..2a79565af 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/RadialDamageEntitySkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/RadialDamageEntitySkill.java @@ -2,6 +2,7 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.entity.damage.DamageType; import org.jetbrains.annotations.NotNull; import org.phantazm.mob.PhantazmMob; @@ -31,12 +32,12 @@ public void use(@NotNull PhantazmMob self) { Iterable entityIterable = (Iterable)iterable; for (LivingEntity entity : entityIterable) { double damage = calculateDamage(entity.getDistance(selfEntity)); - entity.damage(DamageType.fromEntity(selfEntity), (float)damage, data.bypassArmor); + entity.damage(Damage.fromEntity(selfEntity, (float) damage), data.bypassArmor); } } else if (object instanceof LivingEntity living) { double damage = calculateDamage(living.getDistance(selfEntity)); - living.damage(DamageType.fromEntity(selfEntity), (float)damage, data.bypassArmor); + living.damage(Damage.fromEntity(selfEntity, (float) damage), data.bypassArmor); } }); } diff --git a/mob/src/main/java/org/phantazm/mob/skill/RandomSkill.java b/mob/src/main/java/org/phantazm/mob/skill/RandomSkill.java index 273228db1..160c9b7fc 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/RandomSkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/RandomSkill.java @@ -18,12 +18,14 @@ public class RandomSkill implements Skill { private final Data data; private final Skill delegate; private final Random random; + private final boolean needsTicking; @FactoryMethod public RandomSkill(@NotNull Data data, @NotNull @Child("delegate") Skill delegate) { this.data = data; this.delegate = delegate; this.random = new Random(); + this.needsTicking = delegate.needsTicking(); } @Override @@ -37,6 +39,21 @@ public void use(@NotNull PhantazmMob self) { } } + @Override + public void end(@NotNull PhantazmMob self) { + delegate.end(self); + } + + @Override + public boolean needsTicking() { + return needsTicking; + } + + @Override + public void tick(long time, @NotNull PhantazmMob self) { + delegate.tick(time, self); + } + @DataObject public record Data(@Description("Chance that this skill will call its delegate") double chance, @Description("The delegate to call on a successful roll") @NotNull @ChildPath( diff --git a/mob/src/main/java/org/phantazm/mob/skill/RandomTimerSkill.java b/mob/src/main/java/org/phantazm/mob/skill/RandomTimerSkill.java index e7ffb5ba3..1c3b91318 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/RandomTimerSkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/RandomTimerSkill.java @@ -105,6 +105,8 @@ public void end(@NotNull PhantazmMob self) { entity.removeTag(useCountTag); entity.removeTag(startedTag); entity.removeTag(intervalTag); + + delegate.end(self); } private void manageState(PhantazmMob self, int lastUseCount) { diff --git a/mob/src/main/java/org/phantazm/mob/skill/TemporalSkill.java b/mob/src/main/java/org/phantazm/mob/skill/TemporalSkill.java index 951521f4f..35d74ddfd 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/TemporalSkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/TemporalSkill.java @@ -4,6 +4,8 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Entity; import net.minestom.server.tag.Tag; +import net.minestom.server.timer.ExecutionType; +import net.minestom.server.timer.TaskSchedule; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.MathUtils; import org.phantazm.mob.PhantazmMob; @@ -43,7 +45,7 @@ public void tick(long time, @NotNull PhantazmMob self) { return; } - long elapsed = System.currentTimeMillis() - startTime; + long elapsed = time - startTime; if (elapsed / MinecraftServer.TICK_MS >= entity.getTag(this.actualDelay)) { delegate.end(self); @@ -54,13 +56,43 @@ public void tick(long time, @NotNull PhantazmMob self) { @Override public void use(@NotNull PhantazmMob self) { + Entity entity = self.entity(); + long oldStartTime = entity.getTag(this.startTime); + long time = System.currentTimeMillis(); + if (oldStartTime >= 0) { + long elapsed = time - oldStartTime; + if (elapsed / MinecraftServer.TICK_MS < entity.getTag(this.actualDelay)) { + delegate.end(self); + } + } + delegate.use(self); - Entity entity = self.entity(); - entity.setTag(startTime, System.currentTimeMillis()); + entity.setTag(startTime, time); entity.setTag(actualDelay, MathUtils.randomInterval(data.minDuration, data.maxDuration)); } + @Override + public void end(@NotNull PhantazmMob self) { + Entity entity = self.entity(); + long startTime = entity.getTag(this.startTime); + long actualDelay = entity.getTag(this.actualDelay); + if (startTime < 0 || actualDelay < 0) { + delegate.end(self); + return; + } + + long ticksRemaining = actualDelay - ((System.currentTimeMillis() - startTime) / MinecraftServer.TICK_MS); + if (ticksRemaining <= 0) { + delegate.end(self); + return; + } + + MinecraftServer.getSchedulerManager() + .scheduleTask(() -> delegate.end(self), TaskSchedule.tick((int)ticksRemaining), TaskSchedule.stop(), + ExecutionType.SYNC); + } + @Override public boolean needsTicking() { return true; diff --git a/mob/src/main/java/org/phantazm/mob/skill/TimerSkill.java b/mob/src/main/java/org/phantazm/mob/skill/TimerSkill.java index 5665c2782..714ad3d45 100644 --- a/mob/src/main/java/org/phantazm/mob/skill/TimerSkill.java +++ b/mob/src/main/java/org/phantazm/mob/skill/TimerSkill.java @@ -89,6 +89,8 @@ public void end(@NotNull PhantazmMob self) { entity.removeTag(lastActivationTag); entity.removeTag(useCountTag); entity.removeTag(startedTag); + + delegate.end(self); } private void manageState(PhantazmMob self, int lastUseCount) { diff --git a/mob/src/main/java/org/phantazm/mob/spawner/MobSpawner.java b/mob/src/main/java/org/phantazm/mob/spawner/MobSpawner.java index 3ecce7250..338514ec5 100644 --- a/mob/src/main/java/org/phantazm/mob/spawner/MobSpawner.java +++ b/mob/src/main/java/org/phantazm/mob/spawner/MobSpawner.java @@ -8,6 +8,8 @@ import org.phantazm.proxima.bindings.minestom.ProximaEntity; import org.phantazm.proxima.bindings.minestom.Spawner; +import java.util.function.Consumer; + /** * A spawner for {@link PhantazmMob}s. * While {@link Spawner} spawns {@link ProximaEntity}s, this spawns {@link PhantazmMob}s. @@ -23,5 +25,11 @@ public interface MobSpawner { * @param model The {@link MobModel} of the {@link PhantazmMob} to spawn * @return A new {@link PhantazmMob} */ - @NotNull PhantazmMob spawn(@NotNull Instance instance, @NotNull Pos point, @NotNull MobModel model); + @NotNull PhantazmMob spawn(@NotNull Instance instance, @NotNull Pos point, @NotNull MobModel model, + @NotNull Consumer init); + + default @NotNull PhantazmMob spawn(@NotNull Instance instance, @NotNull Pos point, @NotNull MobModel model) { + return spawn(instance, point, model, ignored -> { + }); + } } diff --git a/mob/src/main/java/org/phantazm/mob/target/AITargetSelector.java b/mob/src/main/java/org/phantazm/mob/target/AITargetSelector.java new file mode 100644 index 000000000..ade32e887 --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/target/AITargetSelector.java @@ -0,0 +1,24 @@ +package org.phantazm.mob.target; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.PhantazmMob; + +import java.util.Optional; + +@Model("mob.selector.ai_target") +@Cache +public class AITargetSelector implements TargetSelector { + @FactoryMethod + public AITargetSelector() { + + } + + @Override + public @NotNull Optional selectTarget(@NotNull PhantazmMob self) { + return Optional.ofNullable(self.entity().getTargetEntity()); + } +} diff --git a/mob/src/main/java/org/phantazm/mob/target/AllPlayersSelector.java b/mob/src/main/java/org/phantazm/mob/target/AllPlayersSelector.java index 0b3750b76..2d0b1997a 100644 --- a/mob/src/main/java/org/phantazm/mob/target/AllPlayersSelector.java +++ b/mob/src/main/java/org/phantazm/mob/target/AllPlayersSelector.java @@ -13,7 +13,7 @@ import java.util.Set; @Model("mob.selector.all_players") -@Cache +@Cache(false) public class AllPlayersSelector implements TargetSelector> { private final TargetValidator targetValidator; diff --git a/mob/src/main/java/org/phantazm/mob/target/LastHitEntitySelector.java b/mob/src/main/java/org/phantazm/mob/target/LastHitEntitySelector.java index 73d6e9390..1e2c8ff3b 100644 --- a/mob/src/main/java/org/phantazm/mob/target/LastHitEntitySelector.java +++ b/mob/src/main/java/org/phantazm/mob/target/LastHitEntitySelector.java @@ -4,11 +4,42 @@ import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.instance.EntityTracker; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.Tags; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.proxima.bindings.minestom.ProximaEntity; + +import java.util.Optional; +import java.util.UUID; @Model("mob.selector.last_hit_entity") -@Cache(false) -public class LastHitEntitySelector extends LastHitSelector { +@Cache +public class LastHitEntitySelector implements TargetSelector { @FactoryMethod public LastHitEntitySelector() { } + + @Override + public @NotNull Optional selectTarget(@NotNull PhantazmMob self) { + ProximaEntity entity = self.entity(); + UUID lastHit = entity.getTag(Tags.LAST_MELEE_HIT_TAG); + Instance instance = entity.getInstance(); + + if (lastHit == null || instance == null) { + return Optional.empty(); + } + + for (LivingEntity livingEntity : instance.getEntityTracker().entities(EntityTracker.Target.LIVING_ENTITIES)) { + if (livingEntity.getUuid().equals(lastHit)) { + if (livingEntity.isRemoved()) { + return Optional.empty(); + } + + return Optional.of(livingEntity); + } + } + return Optional.empty(); + } } diff --git a/mob/src/main/java/org/phantazm/mob/target/LastHitSelector.java b/mob/src/main/java/org/phantazm/mob/target/LastHitSelector.java deleted file mode 100644 index 3122a0b07..000000000 --- a/mob/src/main/java/org/phantazm/mob/target/LastHitSelector.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.phantazm.mob.target; - -import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.PhantazmMob; - -import java.util.Optional; - -public class LastHitSelector implements TargetSelector { - - private TTarget lastHit; - - @Override - public @NotNull Optional selectTarget(@NotNull PhantazmMob self) { - return Optional.ofNullable(lastHit); - } - - public void setLastHit(TTarget lastHit) { - this.lastHit = lastHit; - } -} diff --git a/mob/src/main/java/org/phantazm/mob/validator/AlwaysValid.java b/mob/src/main/java/org/phantazm/mob/validator/AlwaysValid.java index 858fcde13..a1900ad0b 100644 --- a/mob/src/main/java/org/phantazm/mob/validator/AlwaysValid.java +++ b/mob/src/main/java/org/phantazm/mob/validator/AlwaysValid.java @@ -5,6 +5,7 @@ import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @Model("mob.target_validator.always_valid") @Cache @@ -14,7 +15,7 @@ public AlwaysValid() { } @Override - public boolean valid(@NotNull Entity targeter, @NotNull Entity entity) { + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { return true; } } diff --git a/mob/src/main/java/org/phantazm/mob/validator/AndValidator.java b/mob/src/main/java/org/phantazm/mob/validator/AndValidator.java index 58901f5b5..150986a99 100644 --- a/mob/src/main/java/org/phantazm/mob/validator/AndValidator.java +++ b/mob/src/main/java/org/phantazm/mob/validator/AndValidator.java @@ -3,6 +3,7 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -17,7 +18,7 @@ public AndValidator(@NotNull @Child("validators") List validato } @Override - public boolean valid(@NotNull Entity targeter, @NotNull Entity entity) { + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { for (TargetValidator validator : validators) { if (!validator.valid(targeter, entity)) { return false; diff --git a/mob/src/main/java/org/phantazm/mob/validator/DistanceValidator.java b/mob/src/main/java/org/phantazm/mob/validator/DistanceValidator.java new file mode 100644 index 000000000..2141369e7 --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/validator/DistanceValidator.java @@ -0,0 +1,33 @@ +package org.phantazm.mob.validator; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Model("mob.target_validator.distance") +@Cache +public class DistanceValidator implements TargetValidator { + private final Data data; + + @FactoryMethod + public DistanceValidator(@NotNull Data data) { + this.data = data; + } + + @Override + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { + if (targeter == null) { + return false; + } + + return targeter.getDistanceSquared(entity) < data.distance * data.distance; + } + + @DataObject + public record Data(double distance) { + } +} diff --git a/mob/src/main/java/org/phantazm/mob/validator/LineOfSightValidator.java b/mob/src/main/java/org/phantazm/mob/validator/LineOfSightValidator.java new file mode 100644 index 000000000..7420bc86c --- /dev/null +++ b/mob/src/main/java/org/phantazm/mob/validator/LineOfSightValidator.java @@ -0,0 +1,25 @@ +package org.phantazm.mob.validator; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Model("mob.target_validator.line_of_sight") +@Cache +public class LineOfSightValidator implements TargetValidator { + @FactoryMethod + public LineOfSightValidator() { + } + + @Override + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { + if (targeter == null) { + return false; + } + + return targeter.hasLineOfSight(entity); + } +} diff --git a/mob/src/main/java/org/phantazm/mob/validator/NotSelfValidator.java b/mob/src/main/java/org/phantazm/mob/validator/NotSelfValidator.java index 56c4b8cf1..dd81b7787 100644 --- a/mob/src/main/java/org/phantazm/mob/validator/NotSelfValidator.java +++ b/mob/src/main/java/org/phantazm/mob/validator/NotSelfValidator.java @@ -4,6 +4,7 @@ import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @Model("mob.target_validator.not_self") public class NotSelfValidator implements TargetValidator { @@ -14,7 +15,7 @@ public NotSelfValidator() { } @Override - public boolean valid(@NotNull Entity targeter, @NotNull Entity entity) { + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { return entity != targeter; } } diff --git a/mob/src/main/java/org/phantazm/mob/validator/OrValidator.java b/mob/src/main/java/org/phantazm/mob/validator/OrValidator.java index ae5b228e5..f9da8c1f3 100644 --- a/mob/src/main/java/org/phantazm/mob/validator/OrValidator.java +++ b/mob/src/main/java/org/phantazm/mob/validator/OrValidator.java @@ -3,6 +3,7 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -17,7 +18,7 @@ public OrValidator(@NotNull @Child("validators") List validator } @Override - public boolean valid(@NotNull Entity targeter, @NotNull Entity entity) { + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { for (TargetValidator validator : validators) { if (validator.valid(targeter, entity)) { return true; diff --git a/mob/src/main/java/org/phantazm/mob/validator/TargetValidator.java b/mob/src/main/java/org/phantazm/mob/validator/TargetValidator.java index b97bc66e5..2d417cf43 100644 --- a/mob/src/main/java/org/phantazm/mob/validator/TargetValidator.java +++ b/mob/src/main/java/org/phantazm/mob/validator/TargetValidator.java @@ -2,7 +2,8 @@ import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public interface TargetValidator { - boolean valid(@NotNull Entity targeter, @NotNull Entity entity); + boolean valid(@Nullable Entity targeter, @NotNull Entity entity); } diff --git a/mob/src/test/java/org/phantazm/mob/target/LastHitSelectorTest.java b/mob/src/test/java/org/phantazm/mob/target/LastHitSelectorTest.java deleted file mode 100644 index 6ab6764b8..000000000 --- a/mob/src/test/java/org/phantazm/mob/target/LastHitSelectorTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.phantazm.mob.target; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.phantazm.mob.PhantazmMob; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; - -public class LastHitSelectorTest { - - private PhantazmMob mob; - - private LastHitSelector lastHitSelector; - - @BeforeEach - public void setup() { - mob = mock(PhantazmMob.class); - lastHitSelector = new LastHitSelector<>(); - } - - @Test - public void testInitiallyEmpty() { - assertTrue(lastHitSelector.selectTarget(mob).isEmpty()); - } - - @Test - public void testUpdatesSingle() { - Object hit = new Object(); - - lastHitSelector.setLastHit(hit); - - Optional target = lastHitSelector.selectTarget(mob); - assertTrue(target.isPresent()); - assertEquals(hit, target.get()); - } - - @Test - public void testUpdatesMultiple() { - Object firstHit = new Object(); - Object secondHit = new Object(); - - lastHitSelector.setLastHit(firstHit); - lastHitSelector.setLastHit(secondHit); - - Optional target = lastHitSelector.selectTarget(mob); - assertTrue(target.isPresent()); - assertEquals(secondHit, target.get()); - } - -} diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/BasicInstanceSpaceHandler.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/BasicInstanceSpaceHandler.java index 6093b6681..bb7a170e0 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/BasicInstanceSpaceHandler.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/BasicInstanceSpaceHandler.java @@ -15,12 +15,11 @@ public class BasicInstanceSpaceHandler implements InstanceSpaceHandler { private final InstanceSpace space; - public BasicInstanceSpaceHandler(@NotNull InstanceSpace instanceSpace) { - this.space = Objects.requireNonNull(instanceSpace, "instanceSpace"); + public BasicInstanceSpaceHandler(@NotNull InstanceSpace space, @NotNull EventNode instanceNode) { + this.space = Objects.requireNonNull(space, "space"); - EventNode node = instanceSpace.instance().eventNode(); - node.addListener(InstanceChunkUnloadEvent.class, this::chunkUnload); - node.addListener(BlockChangeEvent.class, this::blockChange); + instanceNode.addListener(InstanceChunkUnloadEvent.class, this::chunkUnload); + instanceNode.addListener(BlockChangeEvent.class, this::blockChange); } private void chunkUnload(InstanceChunkUnloadEvent event) { diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/GroundPathfindingFactory.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/GroundPathfindingFactory.java index 405c3998e..31b4fb52e 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/GroundPathfindingFactory.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/GroundPathfindingFactory.java @@ -4,10 +4,20 @@ import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import com.github.steanky.proxima.node.Node; import com.github.steanky.proxima.path.Pathfinder; +import com.github.steanky.vector.Vec3D; import com.github.steanky.vector.Vec3I2ObjectMap; -import net.minestom.server.entity.EntityType; +import com.github.steanky.vector.Vec3IBiPredicate; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -24,9 +34,8 @@ public GroundPathfindingFactory(@NotNull Data data) { @Override public @NotNull Pathfinding make(@NotNull Pathfinder pathfinder, - @NotNull ThreadLocal> nodeMapLocal, @NotNull InstanceSpaceHandler spaceHandler, - @NotNull EntityType entityType) { - return new Pathfinding(pathfinder, nodeMapLocal, spaceHandler, entityType) { + @NotNull ThreadLocal> nodeMapLocal, @NotNull InstanceSpaceHandler spaceHandler) { + return new Pathfinding(pathfinder, nodeMapLocal, spaceHandler) { @Override protected float jumpHeight() { return data.jumpHeight; @@ -41,11 +50,61 @@ protected float fallTolerance() { protected float stepHeight() { return data.stepHeight; } + + @Override + protected @NotNull Vec3IBiPredicate successPredicate() { + return data.targetDeviation <= 0 + ? (x1, y1, z1, x2, y2, z2) -> x1 == x2 && y1 == y2 && z1 == z2 + : (x1, y1, z1, x2, y2, z2) -> { + if (Vec3D.distanceSquared(x1 + 0.5, y1, z1 + 0.5, x2 + 0.5, y2, z2 + 0.5) <= + data.targetDeviation * data.targetDeviation) { + if (!data.lineOfSight) { + return true; + } + + Entity self = super.self; + if (self == null) { + return true; + } + + double eyeHeight = self.getEyeHeight(); + Instance instance = self.getInstance(); + Entity target = super.target; + if (instance == null || target == null) { + return true; + } + + Point end = new Vec(x2 + 0.5, y2 + target.getEyeHeight(), z2 + 0.5); + Pos start = new Pos(x1 + 0.5, y1 + eyeHeight, z1 + 0.5).withLookAt(end); + return CollisionUtils.isLineOfSightReachingShape(instance, self.getChunk(), start, end, + target.getBoundingBox()); + } + + return false; + }; + } + + @Override + public boolean useSynthetic() { + return data.targetDeviation <= 0; + } }; } @DataObject - public record Data(float jumpHeight, float fallTolerance, float stepHeight) { + public record Data(float jumpHeight, + float fallTolerance, + float stepHeight, + double targetDeviation, + boolean lineOfSight) { + @Default("targetDeviation") + public static @NotNull ConfigElement defaultTargetDeviation() { + return ConfigPrimitive.of(0.0); + } + @Default("lineOfSight") + public static @NotNull ConfigElement defaultLineOfSight() { + return ConfigPrimitive.of(true); + } } } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSettingsFunction.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSettingsFunction.java index 4a7af5ce0..8a463249c 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSettingsFunction.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSettingsFunction.java @@ -5,8 +5,10 @@ import com.github.steanky.vector.HashVec3I2ObjectMap; import com.github.steanky.vector.Vec3I2ObjectMap; import net.minestom.server.event.Event; +import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.InstanceUnregisterEvent; +import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.WorldBorder; import net.minestom.server.world.DimensionType; @@ -17,9 +19,11 @@ import java.util.function.Function; public class InstanceSettingsFunction implements Function { + private final EventNode rootNode; private final Map settingsMap; public InstanceSettingsFunction(@NotNull EventNode rootNode) { + this.rootNode = rootNode; this.settingsMap = new ConcurrentHashMap<>(); rootNode.addListener(InstanceUnregisterEvent.class, this::onInstanceUnregister); } @@ -48,9 +52,14 @@ public InstanceSettingsFunction(@NotNull EventNode rootNode) { InstanceSpace instanceSpace = new InstanceSpace(instance); ThreadLocal> local = ThreadLocal.withInitial(() -> new HashVec3I2ObjectMap<>(bounds)); - BasicInstanceSpaceHandler instanceSpaceHandler = new BasicInstanceSpaceHandler(instanceSpace); - return new InstanceSpawner.InstanceSettings(local, instanceSpaceHandler); + EventNode node = + EventNode.type("pathfinding_node_" + instance.getUniqueId(), EventFilter.INSTANCE, + (e, v) -> v == instance); + rootNode.addChild(node); + + BasicInstanceSpaceHandler instanceSpaceHandler = new BasicInstanceSpaceHandler(instanceSpace, node); + return new InstanceSpawner.InstanceSettings(local, node, instanceSpaceHandler); }); } @@ -58,6 +67,7 @@ private void onInstanceUnregister(InstanceUnregisterEvent event) { InstanceSpawner.InstanceSettings settings = settingsMap.remove(event.getInstance().getUniqueId()); if (settings != null) { + rootNode.removeChild(settings.instanceNode()); settings.spaceHandler().space().clearCache(); } } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpace.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpace.java index 1e26c2267..949ea2418 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpace.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpace.java @@ -166,7 +166,7 @@ private static Solid[] getSplit(Shape shape) { for (BoundingBox boundingBox : bounds) { if (boundingBox.maxY() > 1) { double minY = boundingBox.minY(); - upper.add(Bounds3D.immutable(boundingBox.minX(), 1, boundingBox.minZ(), boundingBox.width(), + upper.add(Bounds3D.immutable(boundingBox.minX(), 0, boundingBox.minZ(), boundingBox.width(), boundingBox.height() - 1, boundingBox.depth())); if (minY < 1) { diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpawner.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpawner.java index 1069fd126..8436e0df8 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpawner.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/InstanceSpawner.java @@ -5,15 +5,19 @@ import com.github.steanky.vector.Vec3I2ObjectMap; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.UUID; +import java.util.function.Consumer; import java.util.function.Function; public class InstanceSpawner implements Spawner { public record InstanceSettings(@NotNull ThreadLocal> nodeLocal, + @NotNull EventNode instanceNode, @NotNull InstanceSpaceHandler spaceHandler) { } @@ -29,17 +33,18 @@ public InstanceSpawner(@NotNull Pathfinder pathfinder, @Override public @NotNull ProximaEntity spawn(@NotNull Instance instance, @NotNull Pos pos, @NotNull EntityType entityType, - @NotNull Pathfinding.Factory factory) { + @NotNull Pathfinding.Factory factory, @NotNull Consumer init) { InstanceSettings settings = settingsFunction.apply(instance); if (settings == null) { throw new IllegalStateException( "Unable to spawn entity in instance " + instance.getUniqueId() + ", " + "missing InstanceSettings"); } - Pathfinding pathfinding = factory.make(pathfinder, settings.nodeLocal, settings.spaceHandler, entityType); + Pathfinding pathfinding = factory.make(pathfinder, settings.nodeLocal, settings.spaceHandler); ProximaEntity entity = new ProximaEntity(entityType, UUID.randomUUID(), pathfinding); - entity.setInstance(instance, pos); + init.accept(entity); + entity.setInstance(instance, pos); return entity; } } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Pathfinding.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Pathfinding.java index b77faf66f..12eee363d 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Pathfinding.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Pathfinding.java @@ -17,8 +17,10 @@ import com.github.steanky.vector.Vec3D; import com.github.steanky.vector.Vec3I2ObjectMap; import com.github.steanky.vector.Vec3IBiPredicate; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.entity.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.phantazm.proxima.bindings.minestom.controller.Controller; import org.phantazm.proxima.bindings.minestom.controller.GroundController; @@ -27,19 +29,30 @@ import java.util.function.BiPredicate; public class Pathfinding { - public static final double PATH_EPSILON = 1E-3; + public interface Penalty { + Penalty NONE = (x, y, z, h) -> h; + + float calculate(int x, int y, int z, float h); + } + + public static final double PLAYER_PATH_EPSILON = 0.0005; + public static final double MOB_PATH_EPSILON = 1E-6; + public static final double PLAYER_PATH_EPSILON_DOWNWARDS = MOB_PATH_EPSILON; public interface Factory { @NotNull Pathfinding make(@NotNull Pathfinder pathfinder, - @NotNull ThreadLocal> nodeMapLocal, @NotNull InstanceSpaceHandler spaceHandler, - @NotNull EntityType entityType); + @NotNull ThreadLocal> nodeMapLocal, @NotNull InstanceSpaceHandler spaceHandler); } protected final Pathfinder pathfinder; protected final ThreadLocal> nodeMapLocal; protected final InstanceSpaceHandler spaceHandler; - protected final EntityType entityType; + protected Entity self; + protected Entity target; + protected Penalty penalty; + + protected BoundingBox lastBoundingBox; protected Vec3IBiPredicate successPredicate; protected NodeSnapper nodeSnapper; protected PathLimiter pathLimiter; @@ -52,32 +65,82 @@ public interface Factory { protected Controller controller; public Pathfinding(@NotNull Pathfinder pathfinder, @NotNull ThreadLocal> nodeMapLocal, - @NotNull InstanceSpaceHandler spaceHandler, @NotNull EntityType entityType) { + @NotNull InstanceSpaceHandler spaceHandler) { this.pathfinder = Objects.requireNonNull(pathfinder, "pathfinder"); this.nodeMapLocal = Objects.requireNonNull(nodeMapLocal, "nodeMapLocal"); this.spaceHandler = Objects.requireNonNull(spaceHandler, "spaceHandler"); - this.entityType = Objects.requireNonNull(entityType, "entityType"); + this.penalty = Penalty.NONE; + } + + private void clearState() { + this.lastBoundingBox = null; + this.successPredicate = null; + this.nodeSnapper = null; + this.pathLimiter = null; + this.explorer = null; + this.heuristic = null; + this.nodeProcessor = null; + this.navigator = null; + this.pathSettings = null; + this.controller = null; + } + + public void setPenalty(@NotNull Penalty penalty) { + Objects.requireNonNull(penalty, "penalty"); + if (penalty == this.penalty) { + return; + } + + clearState(); + this.penalty = penalty; + } + + public void setSelf(@NotNull Entity self) { + this.self = self; + } + + public void setTarget(@Nullable Entity target) { + this.target = target; + } + + public void cancel() { + Navigator navigator = this.navigator; + if (navigator != null) { + navigator.cancel(); + } } - public @NotNull Navigator getNavigator() { - return Objects.requireNonNullElseGet(navigator, - () -> navigator = new BasicNavigator(pathfinder, getSettings())); + public @NotNull Navigator getNavigator(@NotNull BoundingBox boundingBox) { + if (lastBoundingBox != null && !boundingBox.equals(lastBoundingBox)) { + cancel(); + return navigator = + new BasicNavigator(pathfinder, pathSettings = generateSettings(this.lastBoundingBox = boundingBox)); + } + + return Objects.requireNonNullElseGet(navigator, () -> navigator = + new BasicNavigator(pathfinder, pathSettings = generateSettings(this.lastBoundingBox = boundingBox))); } - public @NotNull PathSettings getSettings() { - return Objects.requireNonNullElseGet(pathSettings, () -> pathSettings = generateSettings()); + public @NotNull PathSettings getSettings(@NotNull BoundingBox boundingBox) { + if (lastBoundingBox != null && !boundingBox.equals(lastBoundingBox)) { + cancel(); + return pathSettings = generateSettings(this.lastBoundingBox = boundingBox); + } + + return Objects.requireNonNullElseGet(pathSettings, + () -> pathSettings = generateSettings(this.lastBoundingBox = boundingBox)); } public @NotNull Controller getController(@NotNull LivingEntity livingEntity) { return Objects.requireNonNullElseGet(controller, - () -> controller = new GroundController(livingEntity, stepHeight())); + () -> controller = new GroundController(livingEntity, stepHeight(), jumpHeight())); } public @NotNull PositionResolver positionResolverForTarget(@NotNull Entity entity) { - EntityType type = entity.getEntityType(); + BoundingBox boundingBox = entity.getBoundingBox(); return PositionResolver.asIfByInitial( - new BasicNodeSnapper(spaceHandler.space(), type.width(), type.height(), fallTolerance(), jumpHeight(), - PATH_EPSILON), 16, type.width(), PATH_EPSILON); + new BasicNodeSnapper(spaceHandler.space(), boundingBox.width(), boundingBox.height(), fallTolerance(), + jumpHeight(), PLAYER_PATH_EPSILON), 16, boundingBox.width(), PLAYER_PATH_EPSILON_DOWNWARDS); } public @NotNull BiPredicate targetChangePredicate(@NotNull Entity entity) { @@ -88,16 +151,16 @@ public boolean isValidTarget(@NotNull Entity targetEntity) { boolean entityValid = !targetEntity.isRemoved() && targetEntity.getInstance() == spaceHandler.instance(); if (entityValid && targetEntity instanceof Player player) { GameMode mode = player.getGameMode(); - return !(mode == GameMode.CREATIVE || mode == GameMode.SPECTATOR); + return mode != GameMode.SPECTATOR; } return entityValid; } - protected @NotNull PathSettings generateSettings() { + protected @NotNull PathSettings generateSettings(@NotNull BoundingBox boundingBox) { Vec3IBiPredicate successPredicate = this.successPredicate = Objects.requireNonNull(successPredicate(), "successPredicate"); - this.nodeSnapper = Objects.requireNonNull(nodeSnapper(), "nodeSnapper"); + this.nodeSnapper = Objects.requireNonNull(nodeSnapper(boundingBox), "nodeSnapper"); this.pathLimiter = Objects.requireNonNull(pathLimiter(), "pathLimiter"); Explorer explorer = this.explorer = Objects.requireNonNull(explorer(), "explorer"); @@ -137,9 +200,9 @@ public boolean isValidTarget(@NotNull Entity targetEntity) { return (x1, y1, z1, x2, y2, z2) -> x1 == x2 && y1 == y2 && z1 == z2; } - protected @NotNull NodeSnapper nodeSnapper() { - return new BasicNodeSnapper(spaceHandler.space(), entityType.width(), entityType.height(), fallTolerance(), - jumpHeight(), PATH_EPSILON); + protected @NotNull NodeSnapper nodeSnapper(@NotNull BoundingBox boundingBox) { + return new BasicNodeSnapper(spaceHandler.space(), boundingBox.width(), boundingBox.height(), fallTolerance(), + jumpHeight(), MOB_PATH_EPSILON); } protected @NotNull Explorer explorer() { @@ -151,13 +214,20 @@ public boolean isValidTarget(@NotNull Entity targetEntity) { } protected @NotNull Heuristic heuristic() { - return Heuristic.DISTANCE_SQUARED; + return (fromX, fromY, fromZ, toX, toY, toZ) -> { + float h = Heuristic.DISTANCE_SQUARED.heuristic(fromX, fromY, fromZ, toX, toY, toZ); + return penalty.calculate(fromX, fromY, fromZ, h); + }; } public boolean canPathfind(@NotNull ProximaEntity proximaEntity) { return proximaEntity.isOnGround(); } + public boolean useSynthetic() { + return true; + } + protected @NotNull NodeProcessor nodeProcessor() { return NodeProcessor.createDiagonals(nodeSnapper); } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/ProximaEntity.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/ProximaEntity.java index d0f0220e0..3352c6452 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/ProximaEntity.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/ProximaEntity.java @@ -4,6 +4,9 @@ import com.github.steanky.proxima.node.Node; import com.github.steanky.proxima.path.PathResult; import com.github.steanky.proxima.path.PathTarget; +import com.github.steanky.proxima.resolver.PositionResolver; +import com.github.steanky.vector.Vec3D; +import com.github.steanky.vector.Vec3I; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -15,27 +18,26 @@ import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.phantazm.commons.MathUtils; import org.phantazm.core.VecUtils; import org.phantazm.proxima.bindings.minestom.controller.Controller; import org.phantazm.proxima.bindings.minestom.goal.GoalGroup; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; /** * An entity with navigation capabilities based on the Proxima library. */ public class ProximaEntity extends LivingEntity { - private static final double NODE_REACH_DISTANCE = 0.4; + private static final double NODE_REACH_DISTANCE_SQ = 0.2; + private static final double NODE_DEVIATION_DISTANCE_SQ = 2.5; + private static final double ENTITY_LOOK_DISTANCE_SQ = 100; protected final Pathfinding pathfinding; protected final List goalGroups; - private Entity targetEntity; - private PathTarget destination; private PathResult currentPath; @@ -56,6 +58,7 @@ public ProximaEntity(@NotNull EntityType entityType, @NotNull UUID uuid, @NotNul super(entityType, uuid); this.pathfinding = Objects.requireNonNull(pathfinding, "pathfinding"); this.goalGroups = new ArrayList<>(5); + pathfinding.setSelf(this); } public @NotNull Pathfinding pathfinding() { @@ -66,12 +69,9 @@ public void setRemovalAnimationDelay(int delay) { this.removalAnimationDelay = delay; } - private void cancelPath() { - this.destination = null; - pathfinding.getNavigator().cancel(); + private void resetPath() { + pathfinding.cancel(); - targetEntity = null; - destination = null; currentPath = null; current = null; @@ -86,9 +86,15 @@ private void cancelPath() { lastZ = 0; } + private void destroyPath() { + resetPath(); + this.destination = null; + pathfinding.target = null; + } + public void setDestination(@Nullable PathTarget destination) { if (destination == null && this.destination != null) { - cancelPath(); + destroyPath(); return; } @@ -96,23 +102,23 @@ public void setDestination(@Nullable PathTarget destination) { return; } + resetPath(); + pathfinding.target = null; this.destination = destination; } public void setDestination(@Nullable T targetEntity) { - if (this.targetEntity == targetEntity) { + if (pathfinding.target == targetEntity) { return; } - if (targetEntity == null || !targetEntity.isRemoved()) { - this.targetEntity = targetEntity; - } - - if (targetEntity == null) { - cancelPath(); + if (targetEntity == null || targetEntity.isRemoved()) { + destroyPath(); return; } + resetPath(); + pathfinding.target = targetEntity; this.destination = PathTarget.resolving(() -> { if (!pathfinding.isValidTarget(targetEntity)) { return null; @@ -123,7 +129,7 @@ public void setDestination(@Nullable T targetEntity) { } public @Nullable Entity getTargetEntity() { - return targetEntity; + return pathfinding.target; } public void attack(@NotNull Entity target, boolean swingHand) { @@ -148,6 +154,10 @@ public void addGoalGroup(@NotNull GoalGroup group) { goalGroups.add(group); } + public @NotNull @Unmodifiable List goalGroups() { + return Collections.unmodifiableList(goalGroups); + } + @Override public void update(long time) { super.update(time); @@ -171,7 +181,7 @@ public void kill() { @Override public void remove() { super.remove(); - cancelPath(); + destroyPath(); } protected boolean canNavigate() { @@ -183,16 +193,15 @@ protected void navigatorTick(long time) { return; } - Navigator navigator = pathfinding.getNavigator(); + Navigator navigator = pathfinding.getNavigator(getBoundingBox()); - if (targetEntity != null && !pathfinding.isValidTarget(targetEntity)) { - targetEntity = null; - cancelPath(); + if (pathfinding.target != null && !pathfinding.isValidTarget(pathfinding.target)) { + destroyPath(); return; } - if (targetEntity != null && getDistanceSquared(targetEntity) < 100) { - lookAt(targetEntity); + if (pathfinding.target != null && getDistanceSquared(pathfinding.target) < ENTITY_LOOK_DISTANCE_SQ) { + lookAt(pathfinding.target); } if (navigator.navigationComplete()) { @@ -201,18 +210,24 @@ protected void navigatorTick(long time) { currentPath = null; } } - else if (destination != null && pathfinding.canPathfind(this) && - (time - lastPathfind > recalculationDelay && destination.hasChanged())) { + else if (destination != null && pathfinding.canPathfind(this) && (time - lastPathfind > recalculationDelay && + (destination.hasChanged() || (currentPath == null || !currentPath.isSuccessful())))) { navigator.navigate(position.x(), position.y(), position.z(), destination); this.lastPathfind = time; } - if (currentPath != null && current != null && moveAlongPath(time)) { - cancelPath(); + if (currentPath != null && current != null) { + if (moveAlongPath(time) != MoveResult.CONTINUE) { + resetPath(); + } } } protected void aiTick(long time) { + if (isDead()) { + return; + } + for (GoalGroup group : goalGroups) { group.tick(time); } @@ -233,15 +248,15 @@ protected boolean initPath(@NotNull PathResult pathResult) { Node closestNode = null; while (node != null) { - double thisDistance = - currentPosition.distanceSquared(node.x + 0.5, node.y + node.blockOffset, node.z + 0.5); + double flatDistance = + Vec3D.distanceSquared(node.x + 0.5, 0, node.z + 0.5, currentPosition.x(), 0, currentPosition.z()); - if (thisDistance < closestNodeDistance) { - closestNodeDistance = thisDistance; + if (flatDistance < closestNodeDistance) { + closestNodeDistance = flatDistance; closestNode = node; } - if (thisDistance < 1) { + if (flatDistance < 1) { break; } @@ -258,13 +273,30 @@ protected boolean initPath(@NotNull PathResult pathResult) { return true; } - protected boolean withinDistance(@NotNull Node node) { + protected boolean withinDistance(@Nullable Node node) { + if (node == null) { + return false; + } + Pos position = getPosition(); return position.distanceSquared(new Vec(node.x + 0.5, node.y + node.blockOffset, node.z + 0.5)) < - NODE_REACH_DISTANCE; + NODE_REACH_DISTANCE_SQ && (int)Math.floor(position.y()) == node.y; + } + + private enum MoveResult { + CONTINUE, + CANCEL, + KEEP_DESTINATION } - protected boolean moveAlongPath(long time) { + protected MoveResult moveAlongPath(long time) { + Point pos = getPosition(); + + if (pos.distanceSquared(current.x + 0.5, current.y + current.blockOffset, current.z + 0.5) > + NODE_DEVIATION_DISTANCE_SQ && (current.parent != null && !current.equals(currentPath.head()))) { + return MoveResult.CANCEL; + } + Controller controller = pathfinding.getController(this); if (withinDistance(target)) { @@ -272,20 +304,34 @@ protected boolean moveAlongPath(long time) { target = current.parent; } - if (target != null) { - Point pos = getPosition(); + Node target = this.target; + if (target == null && pathfinding.target != null && currentPath != null && currentPath.isSuccessful() && + pathfinding.useSynthetic()) { + Vec3I synthetic = PositionResolver.FLOORED.resolve(VecUtils.toDouble(pathfinding.target.getPosition())); + if (!pathfinding.getSettings(getBoundingBox()).successPredicate() + .test(synthetic.x(), synthetic.y(), synthetic.z(), current.x, current.y, current.z)) { + target = new Node(synthetic.x(), synthetic.y(), synthetic.z(), 0, 0, + (float)(pathfinding.target.getPosition().y() - pathfinding.target.getPosition().blockY())); + } + } + + if (target != null) { double currentX = pos.x(); double currentY = pos.y(); double currentZ = pos.z(); + controller.advance(current, target, pathfinding.target); + if (!controller.hasControl()) { - if (!(currentX == lastX && currentY == lastY && currentZ == lastZ)) { + if (!(MathUtils.fuzzyEquals(currentX, lastX, Pathfinding.MOB_PATH_EPSILON) && + MathUtils.fuzzyEquals(currentY, lastY, Pathfinding.MOB_PATH_EPSILON) && + MathUtils.fuzzyEquals(currentZ, lastZ, Pathfinding.MOB_PATH_EPSILON))) { lastMoved = time; } else if (time - lastMoved > pathfinding.immobileThreshold()) { //if we don't have any movement, stop moving along this path - return true; + return MoveResult.CANCEL; } } else { @@ -293,14 +339,12 @@ else if (time - lastMoved > pathfinding.immobileThreshold()) { lastMoved = time; } - controller.advance(current, target, targetEntity); - lastX = currentX; lastY = currentY; lastZ = currentZ; - return false; + return MoveResult.CONTINUE; } - return true; + return MoveResult.KEEP_DESTINATION; } } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Spawner.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Spawner.java index 90764b8ba..efb7a9e4c 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Spawner.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/Spawner.java @@ -5,7 +5,15 @@ import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import java.util.function.Consumer; + public interface Spawner { @NotNull ProximaEntity spawn(@NotNull Instance instance, @NotNull Pos pos, @NotNull EntityType entityType, - @NotNull Pathfinding.Factory factory); + @NotNull Pathfinding.Factory factory, @NotNull Consumer init); + + default @NotNull ProximaEntity spawn(@NotNull Instance instance, @NotNull Pos pos, @NotNull EntityType entityType, + @NotNull Pathfinding.Factory factory) { + return spawn(instance, pos, entityType, factory, ignored -> { + }); + } } diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/controller/GroundController.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/controller/GroundController.java index 7cf50f6c2..53c8d49eb 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/controller/GroundController.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/controller/GroundController.java @@ -24,10 +24,12 @@ import java.util.function.Predicate; public class GroundController implements Controller { + private static final double TARGET_EPSILON = 0.0001; private static final double ENTITY_COLLISION_FACTOR = 10; private final LivingEntity entity; - private final double step; + private final double stepHeight; + private final double jumpHeight; private final TrackerPredicate trackerPredicate; private boolean jumping; @@ -44,12 +46,13 @@ public class GroundController implements Controller { /** * Creates a new GroundController managing the provided entity, using the given step distance and walk speed. * - * @param entity the entity managed by this controller - * @param step the maximum distance the entity may "step up" blocks + * @param entity the entity managed by this controller + * @param stepHeight the maximum distance the entity may "step up" blocks */ - public GroundController(@NotNull LivingEntity entity, float step) { + public GroundController(@NotNull LivingEntity entity, float stepHeight, float jumpHeight) { this.entity = Objects.requireNonNull(entity, "entity"); - this.step = step; + this.stepHeight = stepHeight; + this.jumpHeight = jumpHeight; this.trackerPredicate = new TrackerPredicate(); } @@ -120,9 +123,12 @@ public boolean test(LivingEntity candidate) { @Override public void advance(@NotNull Node current, @NotNull Node target, @Nullable Entity targetEntity) { - Pos entityPos = entity.getPosition(); + Chunk chunk = entity.getChunk(); + if (chunk == null) { + return; + } - double exactTargetY = target.y + target.blockOffset + target.jumpOffset; + Pos entityPos = entity.getPosition(); double dX = (target.x + 0.5) - entityPos.x(); double dZ = (target.z + 0.5) - entityPos.z(); @@ -164,19 +170,20 @@ public void advance(@NotNull Node current, @NotNull Node target, @Nullable Entit double speedX = Math.copySign(Math.min(Math.abs(vX), Math.abs(dX)), dX); double speedZ = Math.copySign(Math.min(Math.abs(vZ), Math.abs(dZ)), dZ); - Chunk chunk = entity.getChunk(); - assert chunk != null; - if (jumping) { if (entityPos.y() > jumpTargetHeight + Vec.EPSILON) { PhysicsResult physics = CollisionUtils.handlePhysics(instance, chunk, entity.getBoundingBox(), - new Pos(entityPos.x(), jumpTargetHeight + Vec.EPSILON, entityPos.z()), + new Pos(entityPos.x(), jumpTargetHeight + TARGET_EPSILON, entityPos.z()), new Vec(speedX, 0, speedZ), null); if (!physics.hasCollision()) { entity.refreshPosition(physics.newPosition().withView(PositionUtils.getLookYaw(dX, dZ), 0)); jumping = false; } + else if (entity.isOnGround()) { + jumping = false; + } + return; } else if (entity.isOnGround()) { @@ -189,58 +196,76 @@ else if (entity.isOnGround()) { } } - if (entity.isOnGround()) { - Vec deltaMove = new Vec(speedX, 0, speedZ); - PhysicsResult physicsResult = CollisionUtils.handlePhysics(instance, chunk, entity.getBoundingBox(), - new Pos(entityPos.x(), entityPos.y() + Vec.EPSILON, entityPos.z()), new Vec(speedX, 0, speedZ), - null); - - Pos pos = physicsResult.newPosition().withView(PositionUtils.getLookYaw(dX, dZ), 0); - - double currentTarget = current.y + current.blockOffset; - if ((entityPos.y() < exactTargetY - Vec.EPSILON || entityPos.y() < currentTarget - Vec.EPSILON) && - physicsResult.hasCollision()) { - double actualDiff = Double.NEGATIVE_INFINITY; - if (currentTarget - Vec.EPSILON > entityPos.y()) { - actualDiff = currentTarget - entityPos.y(); - jumpTargetHeight = currentTarget; - } + if (!entity.isOnGround()) { + return; + } + + Vec deltaMove = new Vec(speedX, 0, speedZ); + PhysicsResult physicsResult = CollisionUtils.handlePhysics(instance, chunk, entity.getBoundingBox(), + new Pos(entityPos.x(), entityPos.y() + Vec.EPSILON, entityPos.z()), new Vec(speedX, 0, speedZ), null); + + Pos pos = physicsResult.newPosition().withView(PositionUtils.getLookYaw(dX, dZ), 0); - double targetDiff = exactTargetY - entityPos.y(); - if (targetDiff > actualDiff) { - actualDiff = targetDiff; - jumpTargetHeight = exactTargetY; + double currentTarget = current.y + current.blockOffset; + double exactTargetY = target.y + target.blockOffset + target.jumpOffset; + if ((entityPos.y() < currentTarget || entityPos.y() < exactTargetY) && physicsResult.hasCollision()) { + double currentDiff = currentTarget - entityPos.y() + TARGET_EPSILON; + double targetDiff = exactTargetY - entityPos.y() + TARGET_EPSILON; + + boolean canReachCurrent = canReach(currentDiff); + boolean canReachTarget = canReach(targetDiff); + + if (canReachTarget) { + if (targetDiff > currentDiff) { + stepOrJump(targetDiff, exactTargetY, speedX, speedZ, chunk, instance, deltaMove, pos); + return; } + } - stepOrJump(actualDiff, speedX, speedZ, instance, deltaMove, pos); + if (canReachCurrent) { + stepOrJump(currentDiff, currentTarget, speedX, speedZ, chunk, instance, deltaMove, pos); return; } - - entity.refreshPosition(pos); } + + entity.refreshPosition(pos); } - private void stepOrJump(double nodeDiff, double speedX, double speedZ, Instance instance, Vec deltaMove, Pos pos) { - if (nodeDiff > step) { - entity.setVelocity( - new Vec(speedX, computeJumpVelocity(nodeDiff), speedZ).mul(MinecraftServer.TICK_PER_SECOND)); - jumping = true; - } - else if (nodeDiff > -Vec.EPSILON && nodeDiff < step + Vec.EPSILON) { + private boolean canReach(double diff) { + return diff < stepHeight + TARGET_EPSILON || diff < jumpHeight + TARGET_EPSILON; + } + + private void stepOrJump(double nodeDiff, double target, double speedX, double speedZ, Chunk chunk, + Instance instance, Vec deltaMove, Pos pos) { + if (nodeDiff - TARGET_EPSILON < stepHeight && nodeDiff - TARGET_EPSILON < jumpHeight) { stepUp(instance, deltaMove, nodeDiff, pos, speedX, speedZ); + return; + } + + Pos entityPos = entity.getPosition(); + PhysicsResult physicsResult = CollisionUtils.handlePhysics(instance, chunk, entity.getBoundingBox(), + new Pos(entityPos.x(), entityPos.y() + Vec.EPSILON + stepHeight, entityPos.z()), + new Vec(speedX, 0, speedZ), null); + if (!physicsResult.hasCollision()) { + entity.refreshPosition(physicsResult.newPosition().withView(pos.yaw(), pos.pitch())); + return; } + + entity.setVelocity(new Vec(speedX, computeJumpVelocity(nodeDiff), speedZ).mul(MinecraftServer.TICK_PER_SECOND)); + jumpTargetHeight = target + TARGET_EPSILON; + jumping = true; } private void stepUp(Instance instance, Vec deltaMove, double nodeDiff, Pos pos, double speedX, double speedZ) { PhysicsResult canStep = CollisionUtils.handlePhysics(instance, entity.getChunk(), entity.getBoundingBox(), - entity.getPosition().add(0, nodeDiff + Vec.EPSILON, 0), deltaMove, null); + entity.getPosition().add(0, nodeDiff, 0), deltaMove, null); if (canStep.hasCollision()) { entity.refreshPosition(pos); return; } - entity.refreshPosition(entity.getPosition().add(speedX, nodeDiff + Vec.EPSILON, speedZ)); + entity.refreshPosition(entity.getPosition().add(speedX, nodeDiff, speedZ)); } @Override diff --git a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/goal/GoalGroup.java b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/goal/GoalGroup.java index 5e366980c..8231e61fc 100644 --- a/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/goal/GoalGroup.java +++ b/proxima-minestom/src/main/java/org/phantazm/proxima/bindings/minestom/goal/GoalGroup.java @@ -1,10 +1,14 @@ package org.phantazm.proxima.bindings.minestom.goal; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import org.phantazm.commons.Tickable; +import java.util.List; import java.util.Optional; public interface GoalGroup extends Tickable { @NotNull Optional currentGoal(); + + @NotNull @Unmodifiable List goals(); } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 9dc7998f7..e3f3d9156 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -11,7 +11,6 @@ repositories { dependencies { implementation(projects.phantazmCore) - implementation(projects.phantazmMessaging) implementation(projects.phantazmMob) implementation(projects.phantazmZombiesMapdata) implementation(projects.phantazmZombies) diff --git a/server/src/main/java/org/phantazm/server/ChatFeature.java b/server/src/main/java/org/phantazm/server/ChatFeature.java index c7f7d5fc2..4daf52693 100644 --- a/server/src/main/java/org/phantazm/server/ChatFeature.java +++ b/server/src/main/java/org/phantazm/server/ChatFeature.java @@ -2,6 +2,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.command.CommandManager; import net.minestom.server.entity.Player; import net.minestom.server.event.Event; @@ -10,8 +11,9 @@ import net.minestom.server.event.player.PlayerLoginEvent; import org.jetbrains.annotations.NotNull; import org.phantazm.core.chat.ChatChannel; +import org.phantazm.core.chat.ChatConfig; import org.phantazm.core.chat.InstanceChatChannel; -import org.phantazm.core.chat.SelfChatChannel; +import org.phantazm.core.chat.command.ChatChannelSendCommand; import org.phantazm.core.chat.command.ChatCommand; import org.phantazm.core.guild.party.Party; import org.phantazm.core.guild.party.PartyChatChannel; @@ -31,8 +33,6 @@ public final class ChatFeature { */ public static final String DEFAULT_CHAT_CHANNEL_NAME = "all"; - public static final String SELF_CHAT_CHANNEL_NAME = "self"; - private ChatFeature() { throw new UnsupportedOperationException(); } @@ -45,7 +45,8 @@ private ChatFeature() { * @param commandManager The {@link CommandManager} to register chat commands to */ static void initialize(@NotNull EventNode node, @NotNull PlayerViewProvider viewProvider, - @NotNull Map parties, @NotNull CommandManager commandManager) { + @NotNull ChatConfig chatConfig, @NotNull Map parties, + @NotNull CommandManager commandManager) { Map channels = new HashMap<>() { @Override public boolean remove(Object key, Object value) { @@ -57,9 +58,14 @@ public boolean remove(Object key, Object value) { } }; - channels.put(DEFAULT_CHAT_CHANNEL_NAME, new InstanceChatChannel(viewProvider)); - channels.put(SelfChatChannel.CHANNEL_NAME, new SelfChatChannel(viewProvider)); - channels.put(PartyChatChannel.CHANNEL_NAME, new PartyChatChannel(parties, viewProvider)); + ChatChannel defaultChannel = new InstanceChatChannel(viewProvider, MiniMessage.miniMessage(), + chatConfig.chatFormats().get(DEFAULT_CHAT_CHANNEL_NAME)); + channels.put(DEFAULT_CHAT_CHANNEL_NAME, defaultChannel); + commandManager.register(ChatChannelSendCommand.chatChannelSend("ac", defaultChannel)); + ChatChannel partyChannel = new PartyChatChannel(parties, viewProvider, MiniMessage.miniMessage(), + chatConfig.chatFormats().get(PartyChatChannel.CHANNEL_NAME)); + channels.put(PartyChatChannel.CHANNEL_NAME, partyChannel); + commandManager.register(ChatChannelSendCommand.chatChannelSend("pc", partyChannel)); Map playerChannels = new HashMap<>(); commandManager.register(new ChatCommand(channels, playerChannels, () -> DEFAULT_CHAT_CHANNEL_NAME)); @@ -79,7 +85,7 @@ public boolean remove(Object key, Object value) { } channel.findAudience(uuid, audience -> { - Component message = channel.formatMessage(event); + Component message = channel.formatMessage(event.getPlayer(), event.getMessage()); audience.sendMessage(message); }, failure -> { player.sendMessage(failure.left()); diff --git a/server/src/main/java/org/phantazm/server/CommandFeature.java b/server/src/main/java/org/phantazm/server/CommandFeature.java index 62899f569..28556241f 100644 --- a/server/src/main/java/org/phantazm/server/CommandFeature.java +++ b/server/src/main/java/org/phantazm/server/CommandFeature.java @@ -4,13 +4,14 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.game.scene.RouterStore; import org.phantazm.core.game.scene.command.QuitCommand; +import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.player.PlayerViewProvider; public class CommandFeature { static void initialize(@NotNull CommandManager commandManager, @NotNull RouterStore routerStore, - @NotNull PlayerViewProvider viewProvider) { - commandManager.register(QuitCommand.quitCommand(routerStore, viewProvider)); + @NotNull PlayerViewProvider viewProvider, @NotNull SceneFallback defaultFallback) { + commandManager.register(QuitCommand.quitCommand(routerStore, viewProvider, defaultFallback)); } } diff --git a/server/src/main/java/org/phantazm/server/ConfigFeature.java b/server/src/main/java/org/phantazm/server/ConfigFeature.java index c079fc94c..f06573702 100644 --- a/server/src/main/java/org/phantazm/server/ConfigFeature.java +++ b/server/src/main/java/org/phantazm/server/ConfigFeature.java @@ -6,15 +6,23 @@ import com.github.steanky.ethylene.core.ConfigCodec; import com.github.steanky.ethylene.core.ConfigHandler; import com.github.steanky.ethylene.core.loader.SyncFileConfigLoader; +import com.github.steanky.ethylene.mapper.MappingProcessorSource; +import com.github.steanky.ethylene.mapper.type.Token; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.chat.ChatConfig; +import org.phantazm.core.guild.party.PartyConfig; +import org.phantazm.server.command.whisper.WhisperConfig; import org.phantazm.server.config.loader.LobbiesConfigProcessor; import org.phantazm.server.config.loader.PathfinderConfigProcessor; import org.phantazm.server.config.loader.ServerConfigProcessor; import org.phantazm.server.config.loader.ShutdownConfigProcessor; import org.phantazm.server.config.lobby.LobbiesConfig; +import org.phantazm.server.config.player.PlayerConfig; import org.phantazm.server.config.server.PathfinderConfig; import org.phantazm.server.config.server.ServerConfig; import org.phantazm.server.config.server.ShutdownConfig; +import org.phantazm.server.config.server.StartupConfig; +import org.phantazm.server.config.zombies.ZombiesConfig; import java.nio.file.Path; @@ -22,6 +30,8 @@ * Entrypoint for configuration-related features. */ public final class ConfigFeature { + + public static final Path PLAYER_CONFIG_PATH = Path.of("./player-config.toml"); /** * The location of the server configuration file. */ @@ -41,6 +51,19 @@ public final class ConfigFeature { */ public static final Path SHUTDOWN_CONFIG_PATH = Path.of("./shutdown-config.toml"); + public static final Path STARTUP_CONFIG_PATH = Path.of("./startup-config.toml"); + + public static final Path PARTY_CONFIG_PATH = Path.of("./party-config.toml"); + + public static final Path WHISPER_CONFIG_PATH = Path.of("./whisper-config.toml"); + + public static final Path CHAT_CONFIG_PATH = Path.of("./chat-config.yml"); + + public static final Path ZOMBIES_CONFIG_PATH = Path.of("./zombies-config.toml"); + + public static final ConfigHandler.ConfigKey PLAYER_CONFIG_KEY = new ConfigHandler.ConfigKey<>( + PlayerConfig.class, "player_config"); + /** * The {@link ConfigHandler.ConfigKey} instance used to refer to the primary {@link ServerConfig} loader. */ @@ -62,7 +85,22 @@ public final class ConfigFeature { * The {@link ConfigHandler.ConfigKey} instance used to refer to the primary {@link PathfinderConfig} loader. */ public static final ConfigHandler.ConfigKey SHUTDOWN_CONFIG_KEY = - new ConfigHandler.ConfigKey<>(ShutdownConfig.class, "pathfinder_config"); + new ConfigHandler.ConfigKey<>(ShutdownConfig.class, "shutdown_config"); + + public static final ConfigHandler.ConfigKey STARTUP_CONFIG_KEY = + new ConfigHandler.ConfigKey<>(StartupConfig.class, "startup_config"); + + public static final ConfigHandler.ConfigKey PARTY_CONFIG_KEY = + new ConfigHandler.ConfigKey<>(PartyConfig.class, "party_config"); + + public static final ConfigHandler.ConfigKey WHISPER_CONFIG_KEY = + new ConfigHandler.ConfigKey<>(WhisperConfig.class, "whisper_config"); + + public static final ConfigHandler.ConfigKey CHAT_CONFIG_KEY = + new ConfigHandler.ConfigKey<>(ChatConfig.class, "chat_config"); + + public static final ConfigHandler.ConfigKey ZOMBIES_CONFIG_KEY = + new ConfigHandler.ConfigKey<>(ZombiesConfig.class, "zombies_config"); private static ConfigHandler handler; @@ -73,25 +111,50 @@ private ConfigFeature() { /** * Initializes server configuration features. Should only be called once from {@link PhantazmServer#main(String[])}. */ - static void initialize() { + static void initialize(@NotNull MappingProcessorSource mappingProcessorSource) { handler = new BasicConfigHandler(); - ConfigCodec codec = new TomlCodec(); + ConfigCodec tomlCodec = new TomlCodec(); + ConfigCodec yamlCodec = new YamlCodec(); + handler.registerLoader(PLAYER_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(PlayerConfig.class)), + PlayerConfig.DEFAULT, PLAYER_CONFIG_PATH, tomlCodec)); + handler.registerLoader(SERVER_CONFIG_KEY, new SyncFileConfigLoader<>(new ServerConfigProcessor(), ServerConfig.DEFAULT, SERVER_CONFIG_PATH, - codec)); + tomlCodec)); handler.registerLoader(LOBBIES_CONFIG_KEY, new SyncFileConfigLoader<>(new LobbiesConfigProcessor(), LobbiesConfig.DEFAULT, LOBBIES_CONFIG_PATH, - new YamlCodec())); + yamlCodec)); handler.registerLoader(PATHFINDER_CONFIG_KEY, new SyncFileConfigLoader<>(new PathfinderConfigProcessor(), PathfinderConfig.DEFAULT, - PATHFINDER_CONFIG_PATH, codec)); + PATHFINDER_CONFIG_PATH, tomlCodec)); handler.registerLoader(SHUTDOWN_CONFIG_KEY, new SyncFileConfigLoader<>(new ShutdownConfigProcessor(), ShutdownConfig.DEFAULT, SHUTDOWN_CONFIG_PATH, - codec)); + tomlCodec)); + + handler.registerLoader(STARTUP_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(StartupConfig.class)), + StartupConfig.DEFAULT, STARTUP_CONFIG_PATH, tomlCodec)); + + handler.registerLoader(PARTY_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(PartyConfig.class)), + PartyConfig.DEFAULT, PARTY_CONFIG_PATH, tomlCodec)); + + handler.registerLoader(WHISPER_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(WhisperConfig.class)), + WhisperConfig.DEFAULT, WHISPER_CONFIG_PATH, tomlCodec)); + + handler.registerLoader(CHAT_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(ChatConfig.class)), + ChatConfig.DEFAULT, CHAT_CONFIG_PATH, yamlCodec)); + + handler.registerLoader(ZOMBIES_CONFIG_KEY, + new SyncFileConfigLoader<>(mappingProcessorSource.processorFor(Token.ofClass(ZombiesConfig.class)), + ZombiesConfig.DEFAULT, ZOMBIES_CONFIG_PATH, tomlCodec)); } /** diff --git a/server/src/main/java/org/phantazm/server/DatapackFeature.java b/server/src/main/java/org/phantazm/server/DatapackFeature.java new file mode 100644 index 000000000..f064f256b --- /dev/null +++ b/server/src/main/java/org/phantazm/server/DatapackFeature.java @@ -0,0 +1,81 @@ +package org.phantazm.server; + +import it.unimi.dsi.fastutil.ints.IntObjectPair; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.world.biomes.Biome; +import net.minestom.server.world.biomes.BiomeManager; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.datapack.Datapack; +import org.phantazm.core.datapack.DatapackLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class DatapackFeature { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatapackFeature.class); + + private DatapackFeature() { + throw new UnsupportedOperationException(); + } + + static void initialize(@NotNull BiomeManager biomeManager) throws IOException { + DatapackLoader loader = new DatapackLoader(); + + LOGGER.info("Loading datapacks"); + List biomes = new ArrayList<>(); + Path datapacksPath = Path.of("./datapacks"); + PathMatcher matcher = datapacksPath.getFileSystem().getPathMatcher("glob:**/*.zip"); + DirectoryStream.Filter filter = entry -> matcher.matches(entry) && Files.isRegularFile(entry); + int loadedDatapacks = 0; + try (DirectoryStream datapacks = Files.newDirectoryStream(datapacksPath, filter)) { + for (Path zipPath : datapacks) { + Datapack datapack; + try { + datapack = loadDatapackFromPath(loader, zipPath); + ++loadedDatapacks; + } + catch (IOException e) { + LOGGER.warn("Failed to load datapack at {}", zipPath, e); + continue; + } + + biomes.addAll(datapack.biomes().values()); + } + } + + LOGGER.info("Loaded {} datapacks", loadedDatapacks); + + NamespaceID plainsID = NamespaceID.from("minecraft:plains"); + biomeManager.removeBiome(Biome.PLAINS); + int id = -1; + boolean hasPlains = false; + + Collection> biomeEntries = new ArrayList<>(biomes.size()); + for (Biome biome : biomes) { + if (biome.name().equals(plainsID)) { + hasPlains = true; + } + + biomeEntries.add(IntObjectPair.of(++id, biome)); + } + if (!hasPlains) { + biomeEntries.add(IntObjectPair.of(++id, Biome.PLAINS)); + } + + biomeManager.addBiomes(biomeEntries); + } + + private static Datapack loadDatapackFromPath(DatapackLoader loader, Path zipPath) throws IOException { + try (FileSystem datapackFileSystem = FileSystems.newFileSystem(zipPath)) { + Path datapackPath = datapackFileSystem.getPath("/"); + return loader.loadDatapack(datapackPath); + } + } + +} diff --git a/server/src/main/java/org/phantazm/server/EquipmentFeature.java b/server/src/main/java/org/phantazm/server/EquipmentFeature.java index 31da1de60..2eab66064 100644 --- a/server/src/main/java/org/phantazm/server/EquipmentFeature.java +++ b/server/src/main/java/org/phantazm/server/EquipmentFeature.java @@ -21,6 +21,7 @@ import org.phantazm.zombies.equipment.EquipmentData; import org.phantazm.zombies.equipment.EquipmentTypes; import org.phantazm.zombies.equipment.gun.*; +import org.phantazm.zombies.equipment.gun.action_bar.ZombiesPlayerActionBarSender; import org.phantazm.zombies.equipment.gun.audience.EntityInstanceAudienceProvider; import org.phantazm.zombies.equipment.gun.audience.PlayerAudienceProvider; import org.phantazm.zombies.equipment.gun.effect.*; @@ -30,7 +31,7 @@ import org.phantazm.zombies.equipment.gun.reload.actionbar.StaticActionBarChooser; import org.phantazm.zombies.equipment.gun.shoot.StateShootTester; import org.phantazm.zombies.equipment.gun.shoot.blockiteration.BasicBlockIteration; -import org.phantazm.zombies.equipment.gun.shoot.blockiteration.WallshotBlockIteration; +import org.phantazm.zombies.equipment.gun.shoot.blockiteration.WallshootingBlockIteration; import org.phantazm.zombies.equipment.gun.shoot.blockiteration.WindowBlockIteration; import org.phantazm.zombies.equipment.gun.shoot.endpoint.BasicShotEndpointSelector; import org.phantazm.zombies.equipment.gun.shoot.fire.HitScanFirer; @@ -38,6 +39,7 @@ import org.phantazm.zombies.equipment.gun.shoot.fire.projectile.PhantazmMobProjectileCollisionFilter; import org.phantazm.zombies.equipment.gun.shoot.fire.projectile.ProjectileFirer; import org.phantazm.zombies.equipment.gun.shoot.handler.*; +import org.phantazm.zombies.equipment.gun.shoot.wallshooting.MapObjectsWallshootingChecker; import org.phantazm.zombies.equipment.gun.target.BasicTargetFinder; import org.phantazm.zombies.equipment.gun.target.entityfinder.directional.AroundEndFinder; import org.phantazm.zombies.equipment.gun.target.entityfinder.directional.BetweenPointsFinder; @@ -53,10 +55,7 @@ import org.phantazm.zombies.equipment.gun.visual.ReloadStackMapper; import org.phantazm.zombies.equipment.perk.BasicPerkCreator; import org.phantazm.zombies.equipment.perk.PerkCreator; -import org.phantazm.zombies.equipment.perk.effect.AddGroupSlotsCreator; -import org.phantazm.zombies.equipment.perk.effect.FlaggingPerkEffectCreator; -import org.phantazm.zombies.equipment.perk.effect.ModifierPerkEffectCreator; -import org.phantazm.zombies.equipment.perk.effect.ShotEffectCreator; +import org.phantazm.zombies.equipment.perk.effect.*; import org.phantazm.zombies.equipment.perk.effect.shot.ApplyAttributeShotEffect; import org.phantazm.zombies.equipment.perk.effect.shot.ApplyFireShotEffect; import org.phantazm.zombies.equipment.perk.equipment.BasicPerkEquipmentCreator; @@ -187,7 +186,10 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(GunLevel.class); contextManager.registerElementClass(EntityInstanceAudienceProvider.class); contextManager.registerElementClass(PlayerAudienceProvider.class); + contextManager.registerElementClass(ZombiesPlayerActionBarSender.class); + contextManager.registerElementClass(AlertNoAmmoEffect.class); contextManager.registerElementClass(AmmoLevelEffect.class); + contextManager.registerElementClass(RecoilEffect.class); contextManager.registerElementClass(PlaySoundEffect.class); contextManager.registerElementClass(ReloadActionBarEffect.class); contextManager.registerElementClass(SendMessageEffect.class); @@ -197,7 +199,7 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(StateReloadTester.class); contextManager.registerElementClass(BasicShotEndpointSelector.class); contextManager.registerElementClass(BasicBlockIteration.class); - contextManager.registerElementClass(WallshotBlockIteration.class); + contextManager.registerElementClass(WallshootingBlockIteration.class); contextManager.registerElementClass(WindowBlockIteration.class); contextManager.registerElementClass(PhantazmMobProjectileCollisionFilter.class); contextManager.registerElementClass(ProjectileFirer.class); @@ -216,6 +218,7 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(PotionShotHandler.class); contextManager.registerElementClass(SlowDownShotHandler.class); contextManager.registerElementClass(SoundShotHandler.class); + contextManager.registerElementClass(MapObjectsWallshootingChecker.class); contextManager.registerElementClass(StateShootTester.class); contextManager.registerElementClass(AroundEndFinder.class); contextManager.registerElementClass(BetweenPointsFinder.class); @@ -237,6 +240,7 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(ModifierPerkEffectCreator.class); contextManager.registerElementClass(AddGroupSlotsCreator.class); contextManager.registerElementClass(ShotEffectCreator.class); + contextManager.registerElementClass(TransactionModifierPerkEffectCreator.class); //ShotEffects contextManager.registerElementClass(ApplyAttributeShotEffect.class); @@ -363,9 +367,12 @@ private Optional loadGun(Pair> pa equipmentModule.getMapStats().setShots(equipmentModule.getMapStats().getShots() + 1); if (!event.shot().headshotTargets().isEmpty()) { - equipmentModule.getMapStats().setHeadshotHits(equipmentModule.getMapStats().getHeadshotHits() + 1); - } else if (!event.shot().regularTargets().isEmpty()) { - equipmentModule.getMapStats().setRegularHits(equipmentModule.getMapStats().getRegularHits() + 1); + equipmentModule.getMapStats() + .setHeadshotHits(equipmentModule.getMapStats().getHeadshotHits() + 1); + } + else if (!event.shot().regularTargets().isEmpty()) { + equipmentModule.getMapStats() + .setRegularHits(equipmentModule.getMapStats().getRegularHits() + 1); } }); diff --git a/server/src/main/java/org/phantazm/server/EthyleneFeature.java b/server/src/main/java/org/phantazm/server/EthyleneFeature.java index acd743f00..ec85ae5e4 100644 --- a/server/src/main/java/org/phantazm/server/EthyleneFeature.java +++ b/server/src/main/java/org/phantazm/server/EthyleneFeature.java @@ -2,6 +2,7 @@ import com.github.steanky.element.core.key.KeyParser; import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.core.collection.ConfigList; import com.github.steanky.ethylene.mapper.MappingProcessorSource; import com.github.steanky.ethylene.mapper.signature.ScalarSignature; import com.github.steanky.ethylene.mapper.signature.Signature; @@ -29,8 +30,10 @@ import net.minestom.server.entity.EntityType; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; import net.minestom.server.particle.Particle; import net.minestom.server.permission.Permission; +import net.minestom.server.potion.PotionEffect; import org.jetbrains.annotations.NotNull; import org.jglrxavpok.hephaistos.nbt.NBTCompound; import org.jglrxavpok.hephaistos.nbt.NBTException; @@ -55,13 +58,14 @@ static void initialize(@NotNull KeyParser keyParser) { mappingProcessorSource = MappingProcessorSource.builder().withCustomSignature(vec3I()).withCustomSignature(sound()) - .withCustomSignature(style()).withCustomSignature(textColor()).withCustomSignature(vec3D()) - .withCustomSignature(pos()).withCustomSignature(bounds3D()).withCustomSignature(rgbLike()) - .withScalarSignature(key()).withScalarSignature(uuid()).withScalarSignature(component()) - .withScalarSignature(itemStack()).withScalarSignature(titlePartComponent()) - .withScalarSignature(namedTextColor()).withScalarSignature(particle()) - .withScalarSignature(block()).withScalarSignature(permission()) - .withScalarSignature(entityType()) + .withCustomSignature(basicItemStack()).withCustomSignature(style()) + .withCustomSignature(textColor()).withCustomSignature(vec3D()).withCustomSignature(pos()) + .withCustomSignature(bounds3D()).withCustomSignature(rgbLike()).withScalarSignature(key()) + .withScalarSignature(uuid()).withScalarSignature(component()).withScalarSignature(itemStack()) + .withScalarSignature(titlePartComponent()).withScalarSignature(namedTextColor()) + .withScalarSignature(particle()).withScalarSignature(block()).withScalarSignature(permission()) + .withScalarSignature(entityType()).withScalarSignature(material()) + .withScalarSignature(potionEffect()) .withTypeImplementation(Object2IntOpenHashMap.class, Object2IntMap.class) .withTypeImplementation(IntOpenHashSet.class, IntSet.class).withStandardSignatures() .withStandardTypeImplementations().ignoringLengths().build(); @@ -69,6 +73,37 @@ static void initialize(@NotNull KeyParser keyParser) { LOGGER.info("Ethylene initialized."); } + @SuppressWarnings("unchecked") + private static Signature basicItemStack() { + return Signature.builder(Token.ofClass(ItemStack.class), (ignored, args) -> { + ItemStack.Builder builder = ItemStack.builder(args.get(0)); + String meta = args.get(3); + if (meta != null) { + try { + builder.meta((NBTCompound)new SNBTParser(new StringReader(meta)).parse()); + } + catch (NBTException ignored1) { + } + } + + builder.displayName(args.get(1)).lore((List)args.get(2)); + return builder.build(); + }, itemStack -> { + List list = new ArrayList<>(3); + list.add(itemStack.material()); + list.add(itemStack.getDisplayName()); + list.add(itemStack.getLore()); + list.add(itemStack.meta().toSNBT()); + + return list; + }, Map.entry("material", SignatureParameter.parameter(Token.ofClass(Material.class))), Map.entry("displayName", + SignatureParameter.parameter(Token.ofClass(Component.class), ConfigPrimitive.NULL)), + Map.entry("lore", SignatureParameter.parameter(new Token>() { + }, ConfigList.of())), + Map.entry("tag", SignatureParameter.parameter(Token.STRING, ConfigPrimitive.NULL))).matchingNames() + .matchingTypeHints().build(); + } + private static Signature pos() { return Signature.builder(Token.ofClass(Pos.class), (ignored, args) -> new Pos(args.get(0), args.get(1), args.get(2), args.get(3), args.get(4)), @@ -160,6 +195,12 @@ private static Signature textColor() { .build(); } + private static ScalarSignature potionEffect() { + return ScalarSignature.of(Token.ofClass(PotionEffect.class), + effect -> PotionEffect.fromNamespaceId(effect.asString()), + effect -> effect == null ? ConfigPrimitive.NULL : ConfigPrimitive.of(effect.namespace().asString())); + } + private static ScalarSignature permission() { return ScalarSignature.of(Token.ofClass(Permission.class), element -> new Permission(element.asString()), permission -> permission == null @@ -167,6 +208,12 @@ private static ScalarSignature permission() { : ConfigPrimitive.of(permission.getPermissionName())); } + private static ScalarSignature material() { + return ScalarSignature.of(Token.ofClass(Material.class), + element -> Material.fromNamespaceId(element.asString()), + material -> material == null ? ConfigPrimitive.NULL : ConfigPrimitive.of(material.key().toString())); + } + private static ScalarSignature particle() { return ScalarSignature.of(Token.ofClass(Particle.class), element -> Particle.fromNamespaceId(element.asString()), diff --git a/server/src/main/java/org/phantazm/server/ExecutorFeature.java b/server/src/main/java/org/phantazm/server/ExecutorFeature.java new file mode 100644 index 000000000..f77ed86b0 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/ExecutorFeature.java @@ -0,0 +1,57 @@ +package org.phantazm.server; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class ExecutorFeature { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorFeature.class); + + private static ExecutorService executorService; + + private ExecutorFeature() { + executorService.shutdown(); + } + + static void initialize() { + executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + } + + static void shutdown() { + if (executorService == null) { + return; + } + + try { + LOGGER.info( + "Shutting down executor. Please allow for one minute before shutdown completes."); + executorService.shutdown(); + if (!executorService.awaitTermination(1L, TimeUnit.MINUTES)) { + executorService.shutdownNow(); + + LOGGER.warn( + "Not all tasks completed. Please allow for one minute for tasks to be canceled."); + if (!executorService.awaitTermination(1L, TimeUnit.MINUTES)) { + LOGGER.warn("Database tasks failed to cancel."); + } + } + } + catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + public static @NotNull Executor getExecutor() { + return FeatureUtils.check(executorService); + } +} diff --git a/server/src/main/java/org/phantazm/server/GeneralStatsFeature.java b/server/src/main/java/org/phantazm/server/GeneralStatsFeature.java new file mode 100644 index 000000000..d5873ae5e --- /dev/null +++ b/server/src/main/java/org/phantazm/server/GeneralStatsFeature.java @@ -0,0 +1,42 @@ +package org.phantazm.server; + +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.player.PlayerSpawnEvent; +import org.jetbrains.annotations.NotNull; +import org.phantazm.stats.general.GeneralDatabase; +import org.phantazm.stats.general.JooqGeneralSQLFetcher; +import org.phantazm.stats.general.SQLGeneralDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; + +public class GeneralStatsFeature { + + private static final Logger LOGGER = LoggerFactory.getLogger(GeneralStatsFeature.class); + + private static GeneralDatabase generalDatabase; + + static void initialize(@NotNull EventNode eventNode) { + JooqGeneralSQLFetcher sqlFetcher = new JooqGeneralSQLFetcher(); + generalDatabase = + new SQLGeneralDatabase(ExecutorFeature.getExecutor(), HikariFeature.getDataSource(), sqlFetcher); + + eventNode.addListener(PlayerSpawnEvent.class, GeneralStatsFeature::onPlayerSpawn); + } + + private static void onPlayerSpawn(PlayerSpawnEvent event) { + if (!event.isFirstSpawn()) { + return; + } + + generalDatabase.handleJoin(event.getPlayer().getUuid(), ZonedDateTime.now()) + .whenComplete((ignored, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to update join times for {}", event.getPlayer().getUuid(), throwable); + } + }); + } + +} diff --git a/server/src/main/java/org/phantazm/server/HikariFeature.java b/server/src/main/java/org/phantazm/server/HikariFeature.java new file mode 100644 index 000000000..aeb85f38c --- /dev/null +++ b/server/src/main/java/org/phantazm/server/HikariFeature.java @@ -0,0 +1,26 @@ +package org.phantazm.server; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +public class HikariFeature { + + private static HikariDataSource dataSource; + + static void initialize() { + HikariConfig config = new HikariConfig("./hikari.properties"); + dataSource = new HikariDataSource(config); + } + + public static @NotNull HikariDataSource getDataSource() { + return FeatureUtils.check(dataSource); + } + + public static void end() { + if (dataSource != null) { + dataSource.close(); + } + } + +} diff --git a/server/src/main/java/org/phantazm/server/LobbyFeature.java b/server/src/main/java/org/phantazm/server/LobbyFeature.java index fd3ea7c47..b0795d955 100644 --- a/server/src/main/java/org/phantazm/server/LobbyFeature.java +++ b/server/src/main/java/org/phantazm/server/LobbyFeature.java @@ -2,6 +2,7 @@ import com.github.steanky.element.core.context.ContextManager; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.MinecraftServer; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; @@ -59,7 +60,8 @@ static void initialize(@NotNull EventNode node, @NotNull PlayerViewProvid InstanceManager instanceManager = MinecraftServer.getInstanceManager(); FileUtils.createDirectories(lobbiesConfig.instancesPath()); InstanceLoader instanceLoader = - new AnvilFileSystemInstanceLoader(instanceManager, lobbiesConfig.instancesPath(), DynamicChunk::new); + new AnvilFileSystemInstanceLoader(instanceManager, lobbiesConfig.instancesPath(), DynamicChunk::new, + ExecutorFeature.getExecutor()); SceneFallback finalFallback = new KickFallback(lobbiesConfig.kickMessage()); Map> lobbyProviders = @@ -78,21 +80,24 @@ static void initialize(@NotNull EventNode node, @NotNull PlayerViewProvid } SceneProvider mainLobbyProvider = - new BasicLobbyProvider(mainLobbyConfig.maxLobbies(), -mainLobbyConfig.maxPlayers(), instanceLoader, - mainLobbyConfig.lobbyPaths(), finalFallback, mainLobbyConfig.instanceConfig(), contextManager, - mainLobbyConfig.npcs(), false); + new BasicLobbyProvider(ExecutorFeature.getExecutor(), mainLobbyConfig.maxLobbies(), + -mainLobbyConfig.maxPlayers(), instanceLoader, mainLobbyConfig.lobbyPaths(), finalFallback, + mainLobbyConfig.instanceConfig(), contextManager, mainLobbyConfig.npcs(), + mainLobbyConfig.defaultItems(), MiniMessage.miniMessage(), mainLobbyConfig.lobbyJoinFormat(), + false, node); lobbyProviders.put(lobbiesConfig.mainLobbyName(), mainLobbyProvider); - fallback = new LobbyRouterFallback(MinecraftServer.getConnectionManager(), LobbyFeature.getLobbyRouter(), - lobbiesConfig.mainLobbyName()); + fallback = new LobbyRouterFallback(LobbyFeature.getLobbyRouter(), lobbiesConfig.mainLobbyName()); SceneFallback regularFallback = new CompositeFallback(List.of(fallback, finalFallback)); for (Map.Entry lobby : lobbiesConfig.lobbies().entrySet()) { if (!lobby.getKey().equals(lobbiesConfig.mainLobbyName())) { lobbyProviders.put(lobby.getKey(), - new BasicLobbyProvider(lobby.getValue().maxLobbies(), -lobby.getValue().maxPlayers(), - instanceLoader, lobby.getValue().lobbyPaths(), regularFallback, - lobby.getValue().instanceConfig(), contextManager, mainLobbyConfig.npcs(), true)); + new BasicLobbyProvider(ExecutorFeature.getExecutor(), lobby.getValue().maxLobbies(), + -lobby.getValue().maxPlayers(), instanceLoader, lobby.getValue().lobbyPaths(), + regularFallback, lobby.getValue().instanceConfig(), contextManager, + mainLobbyConfig.npcs(), mainLobbyConfig.defaultItems(), MiniMessage.miniMessage(), + lobby.getValue().lobbyJoinFormat(), true, node)); } } @@ -102,20 +107,23 @@ static void initialize(@NotNull EventNode node, @NotNull PlayerViewProvid LoginLobbyJoinRequest joinRequest = new LoginLobbyJoinRequest(event, playerViewProvider); LobbyRouteRequest routeRequest = new LobbyRouteRequest(lobbiesConfig.mainLobbyName(), joinRequest); - RouteResult routeResult = lobbyRouter.findScene(routeRequest); + RouteResult routeResult = lobbyRouter.findScene(routeRequest).join(); boolean success = false; if (routeResult.scene().isPresent()) { Lobby lobby = routeResult.scene().get(); - TransferResult transferResult = lobby.join(joinRequest); - if (transferResult.executor().isPresent()) { - transferResult.executor().get().run(); - loginJoinRequests.put(event.getPlayer().getUuid(), joinRequest); - success = true; + try (TransferResult transferResult = lobby.join(joinRequest)) { + if (transferResult.executor().isPresent()) { + transferResult.executor().get().run(); + loginJoinRequests.put(event.getPlayer().getUuid(), joinRequest); + success = true; + } } } if (!success) { - finalFallback.fallback(playerViewProvider.fromPlayer(event.getPlayer())); + LOGGER.warn("Kicking player {} because we weren't able to find a lobby for them to join", + event.getPlayer().getUuid()); + event.setCancelled(true); } }); diff --git a/server/src/main/java/org/phantazm/server/MessagingFeature.java b/server/src/main/java/org/phantazm/server/MessagingFeature.java deleted file mode 100644 index c060a0313..000000000 --- a/server/src/main/java/org/phantazm/server/MessagingFeature.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.phantazm.server; - -import net.kyori.adventure.key.Key; -import net.minestom.server.entity.Player; -import net.minestom.server.event.Event; -import net.minestom.server.event.EventNode; -import net.minestom.server.event.player.PlayerPluginMessageEvent; -import net.minestom.server.utils.binary.BinaryReader; -import net.minestom.server.utils.binary.BinaryWriter; -import org.jetbrains.annotations.NotNull; -import org.phantazm.commons.Namespaces; -import org.phantazm.messaging.MessageChannels; -import org.phantazm.messaging.packet.Packet; -import org.phantazm.messaging.packet.PacketHandler; -import org.phantazm.messaging.serialization.PacketSerializer; -import org.phantazm.messaging.serialization.PacketSerializers; -import org.phantazm.server.config.server.AuthType; -import org.phantazm.server.packet.BinaryDataReader; -import org.phantazm.server.packet.BinaryDataWriter; - -import java.util.Map; - -/** - * Main entrypoint for plugin messaging. This is used to communicate with the proxy and players. - */ -public final class MessagingFeature { - - private MessagingFeature() { - throw new UnsupportedOperationException(); - } - - static void initialize(@NotNull EventNode global, @NotNull AuthType authType) { - PacketSerializer clientToServer = - PacketSerializers.clientToServerSerializer(() -> new BinaryDataWriter(new BinaryWriter()), - data -> new BinaryDataReader(new BinaryReader(data))); - Key clientToServerIdentifier = Key.key(Namespaces.PHANTAZM, MessageChannels.CLIENT_TO_SERVER); - Map> packetHandlers = - Map.of(MessageChannels.CLIENT_TO_SERVER, new PacketHandler<>(clientToServer) { - @Override - protected void handlePacket(@NotNull Player player, @NotNull Packet packet) { - - } - - @Override - protected void sendToReceiver(@NotNull Player player, byte @NotNull [] data) { - player.sendPluginMessage(clientToServerIdentifier.toString(), data); - } - }); - - global.addListener(PlayerPluginMessageEvent.class, event -> { - String identifier = event.getIdentifier(); - if (!identifier.contains(":")) { - return; - } - - String[] split = identifier.split(":"); - if (split.length != 2 || !split[0].equals(Namespaces.PHANTAZM)) { - return; - } - - PacketHandler packetHandler = packetHandlers.get(split[1]); - if (packetHandler == null) { - return; - } - - packetHandler.handleData(event.getPlayer(), event.getMessage()); - }); - } - -} diff --git a/server/src/main/java/org/phantazm/server/MobFeature.java b/server/src/main/java/org/phantazm/server/MobFeature.java index a5ba2052b..39213a905 100644 --- a/server/src/main/java/org/phantazm/server/MobFeature.java +++ b/server/src/main/java/org/phantazm/server/MobFeature.java @@ -28,11 +28,13 @@ import org.phantazm.mob.goal.*; import org.phantazm.mob.skill.*; import org.phantazm.mob.target.*; -import org.phantazm.mob.validator.AlwaysValid; -import org.phantazm.mob.validator.AndValidator; -import org.phantazm.mob.validator.NotSelfValidator; -import org.phantazm.mob.validator.OrValidator; +import org.phantazm.mob.validator.*; import org.phantazm.zombies.mob.goal.BreakNearbyWindowGoal; +import org.phantazm.zombies.mob.skill.AttributeModifyingSkill; +import org.phantazm.zombies.mob.skill.ShootProjectileSkill; +import org.phantazm.zombies.mob.skill.SummonMobSkill; +import org.phantazm.zombies.mob.skill.hit_action.AttributeModifierAction; +import org.phantazm.zombies.mob.skill.hit_action.DamageAction; import org.phantazm.zombies.mob.validator.MobValidator; import org.phantazm.zombies.mob.validator.ZombiesPlayerValidator; import org.slf4j.Logger; @@ -123,7 +125,6 @@ private static void registerElementClasses(@NotNull ContextManager contextManage //mob goals contextManager.registerElementClass(FollowEntityGoal.class); contextManager.registerElementClass(ChargeAtEntityGoal.class); - contextManager.registerElementClass(UseSkillGoal.class); contextManager.registerElementClass(MeleeAttackGoal.class); contextManager.registerElementClass(PlayStepSoundGoal.class); @@ -131,16 +132,24 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(BreakNearbyWindowGoal.class); //mob skills + contextManager.registerElementClass(AddVelocitySkill.class); + contextManager.registerElementClass(ApplyPotionSkill.class); contextManager.registerElementClass(BleedEntitiesSkill.class); contextManager.registerElementClass(DamageEntitySkill.class); contextManager.registerElementClass(KnockbackEntitySkill.class); contextManager.registerElementClass(PlaySoundSkill.class); - contextManager.registerElementClass(AttributeModifyingSkill.class); + contextManager.registerElementClass(PushEntitySkill.class); contextManager.registerElementClass(SendMessageSkill.class); contextManager.registerElementClass(JumpTowardsTargetSkill.class); contextManager.registerElementClass(SpawnParticleSkill.class); contextManager.registerElementClass(RadialDamageEntitySkill.class); + contextManager.registerElementClass(SummonMobSkill.class); + contextManager.registerElementClass(AttributeModifyingSkill.class); + contextManager.registerElementClass(ShootProjectileSkill.class); + contextManager.registerElementClass(DamageAction.class); + contextManager.registerElementClass(AttributeModifierAction.class); + //mob meta skills contextManager.registerElementClass(TimerSkill.class); contextManager.registerElementClass(RandomSkill.class); @@ -155,12 +164,15 @@ private static void registerElementClasses(@NotNull ContextManager contextManage contextManager.registerElementClass(LastHitEntitySelector.class); contextManager.registerElementClass(AllPlayersSelector.class); contextManager.registerElementClass(NearestEntitiesSelector.class); + contextManager.registerElementClass(AITargetSelector.class); //mob validators contextManager.registerElementClass(AlwaysValid.class); contextManager.registerElementClass(AndValidator.class); contextManager.registerElementClass(OrValidator.class); contextManager.registerElementClass(NotSelfValidator.class); + contextManager.registerElementClass(LineOfSightValidator.class); + contextManager.registerElementClass(DistanceValidator.class); //zombies mob validators contextManager.registerElementClass(ZombiesPlayerValidator.class); diff --git a/server/src/main/java/org/phantazm/server/NPCFeature.java b/server/src/main/java/org/phantazm/server/NPCFeature.java index 661415bbe..bfb1440b0 100644 --- a/server/src/main/java/org/phantazm/server/NPCFeature.java +++ b/server/src/main/java/org/phantazm/server/NPCFeature.java @@ -5,7 +5,10 @@ import org.phantazm.core.npc.AnimationTicker; import org.phantazm.core.npc.EntityNPC; import org.phantazm.core.npc.NoTicker; +import org.phantazm.core.npc.interactor.MessageInteractor; import org.phantazm.core.npc.interactor.NoInteractor; +import org.phantazm.core.npc.interactor.ShowGuiInteractor; +import org.phantazm.core.npc.interactor.item.InteractorDelegatingItem; import org.phantazm.core.npc.settings.BasicEntitySettings; import org.phantazm.core.npc.supplier.MobEntitySupplier; import org.phantazm.core.npc.interactor.CommandInteractor; @@ -18,6 +21,7 @@ private NPCFeature() { static void initialize(@NotNull ContextManager contextManager) { contextManager.registerElementClass(CommandInteractor.class); + contextManager.registerElementClass(MessageInteractor.class); contextManager.registerElementClass(NoInteractor.class); contextManager.registerElementClass(EntityNPC.class); contextManager.registerElementClass(MobEntitySupplier.class); @@ -26,5 +30,7 @@ static void initialize(@NotNull ContextManager contextManager) { contextManager.registerElementClass(AnimationTicker.class); contextManager.registerElementClass(AnimationTicker.Frame.class); contextManager.registerElementClass(NoTicker.class); + contextManager.registerElementClass(InteractorDelegatingItem.class); + contextManager.registerElementClass(ShowGuiInteractor.class); } } diff --git a/server/src/main/java/org/phantazm/server/PartyFeature.java b/server/src/main/java/org/phantazm/server/PartyFeature.java index 38350c347..eaebb6bcb 100644 --- a/server/src/main/java/org/phantazm/server/PartyFeature.java +++ b/server/src/main/java/org/phantazm/server/PartyFeature.java @@ -4,9 +4,6 @@ import com.github.steanky.ethylene.core.ConfigCodec; import com.github.steanky.ethylene.core.ConfigElement; import com.github.steanky.ethylene.core.bridge.Configuration; -import com.github.steanky.ethylene.core.processor.ConfigProcessor; -import com.github.steanky.ethylene.mapper.MappingProcessorSource; -import com.github.steanky.ethylene.mapper.type.Token; import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.command.CommandManager; import net.minestom.server.command.builder.Command; @@ -22,41 +19,40 @@ import org.phantazm.core.time.TickFormatter; import java.io.IOException; -import java.nio.file.Path; import java.util.*; public class PartyFeature { private static final GuildHolder partyHolder = new GuildHolder<>(new HashMap<>(), new ArrayList<>()); + private static PartyConfig config; + private PartyFeature() { throw new UnsupportedOperationException(); } static void initialize(@NotNull CommandManager commandManager, @NotNull PlayerViewProvider viewProvider, - @NotNull SchedulerManager schedulerManager, @NotNull MappingProcessorSource mappingProcessorSource, - @NotNull ContextManager contextManager, @NotNull ConfigCodec partyCodec, @NotNull MiniMessage miniMessage) + @NotNull SchedulerManager schedulerManager, @NotNull ContextManager contextManager, + @NotNull PartyConfig config, @NotNull ConfigCodec partyCodec, @NotNull MiniMessage miniMessage) throws IOException { - ConfigProcessor partyConfigProcessor = - mappingProcessorSource.processorFor(Token.ofClass(PartyConfig.class)); - String partyFileName = - partyCodec.getPreferredExtensions().isEmpty() ? "party" : "party." + partyCodec.getPreferredExtension(); - ConfigElement partyConfigNode = Configuration.read(Path.of(partyFileName), partyCodec); + PartyFeature.config = config; + ConfigElement partyConfigNode = Configuration.read(ConfigFeature.PARTY_CONFIG_PATH, partyCodec); TickFormatter tickFormatter = contextManager.makeContext(partyConfigNode.getNodeOrThrow("tickFormatter")).provide(); - PartyConfig partyConfig = partyConfigProcessor.dataFromElement(partyConfigNode); - PartyCreator partyCreator = new PartyCreator.Builder().setNotificationConfig(partyConfig.notificationConfig()) - .setTickFormatter(tickFormatter).setMiniMessage(miniMessage).setCreatorRank(partyConfig.creatorRank()) - .setDefaultRank(partyConfig.defaultRank()).setInvitationDuration(partyConfig.invitationDuration()) - .setMinimumKickRank(partyConfig.minimumKickRank()).setMinimumInviteRank(partyConfig.minimumInviteRank()) - .setMinimumJoinRank(partyConfig.minimumJoinRank()).build(); + PartyCreator partyCreator = new PartyCreator.Builder().setNotificationConfig(config.notificationConfig()) + .setTickFormatter(tickFormatter).setMiniMessage(miniMessage).setCreatorRank(config.creatorRank()) + .setDefaultRank(config.defaultRank()).setInvitationDuration(config.invitationDuration()) + .setMinimumKickRank(config.minimumKickRank()).setMinimumInviteRank(config.minimumInviteRank()) + .setMinimumJoinRank(config.minimumJoinRank()).build(); Command partyCommand = - PartyCommand.partyCommand(partyConfig.commandConfig(), miniMessage, partyHolder, viewProvider, - partyCreator, new Random()); + PartyCommand.partyCommand(config.commandConfig(), miniMessage, partyHolder, viewProvider, partyCreator, + new Random(), config.creatorRank()); commandManager.register(partyCommand); schedulerManager.scheduleTask(() -> { + partyHolder.guilds().removeIf(party -> party.getMemberManager().getMembers().isEmpty()); + Set ticked = Collections.newSetFromMap(new IdentityHashMap<>()); long time = System.currentTimeMillis(); for (Party party : partyHolder.guilds()) { @@ -70,4 +66,8 @@ static void initialize(@NotNull CommandManager commandManager, @NotNull PlayerVi public static @NotNull GuildHolder getPartyHolder() { return partyHolder; } + + public static @NotNull PartyConfig getConfig() { + return FeatureUtils.check(config); + } } diff --git a/server/src/main/java/org/phantazm/server/PhantazmServer.java b/server/src/main/java/org/phantazm/server/PhantazmServer.java index de76c2461..4f738c47e 100644 --- a/server/src/main/java/org/phantazm/server/PhantazmServer.java +++ b/server/src/main/java/org/phantazm/server/PhantazmServer.java @@ -20,22 +20,24 @@ import net.minestom.server.event.server.ServerListPingEvent; import net.minestom.server.extras.MojangAuth; import net.minestom.server.extras.bungee.BungeeCordProxy; -import net.minestom.server.extras.optifine.OptifineSupport; import net.minestom.server.extras.velocity.VelocityProxy; import org.jetbrains.annotations.Nullable; import org.phantazm.commons.Namespaces; +import org.phantazm.core.chat.ChatConfig; import org.phantazm.core.game.scene.BasicRouterStore; import org.phantazm.core.game.scene.RouterStore; +import org.phantazm.core.game.scene.SceneTransferHelper; import org.phantazm.core.game.scene.fallback.CompositeFallback; import org.phantazm.core.game.scene.fallback.KickFallback; +import org.phantazm.core.guild.party.PartyConfig; import org.phantazm.core.player.BasicPlayerViewProvider; import org.phantazm.core.player.IdentitySource; import org.phantazm.core.player.PlayerViewProvider; +import org.phantazm.server.command.whisper.WhisperConfig; import org.phantazm.server.config.lobby.LobbiesConfig; -import org.phantazm.server.config.server.PathfinderConfig; -import org.phantazm.server.config.server.ServerConfig; -import org.phantazm.server.config.server.ServerInfoConfig; -import org.phantazm.server.config.server.ShutdownConfig; +import org.phantazm.server.config.player.PlayerConfig; +import org.phantazm.server.config.server.*; +import org.phantazm.server.config.zombies.ZombiesConfig; import org.phantazm.server.player.BasicLoginValidator; import org.phantazm.server.player.LoginValidator; import org.phantazm.zombies.equipment.EquipmentData; @@ -47,6 +49,7 @@ import org.snakeyaml.engine.v2.api.LoadSettings; import org.snakeyaml.engine.v2.common.FlowStyle; +import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Set; @@ -79,13 +82,23 @@ public final class PhantazmServer { public static void main(String[] args) { MinecraftServer minecraftServer = MinecraftServer.init(); + PlayerConfig playerConfig; ServerConfig serverConfig; LobbiesConfig lobbiesConfig; PathfinderConfig pathfinderConfig; ShutdownConfig shutdownConfig; + StartupConfig startupConfig; + PartyConfig partyConfig; + WhisperConfig whisperConfig; + ChatConfig chatConfig; + ZombiesConfig zombiesConfig; + + KeyParser keyParser = new BasicKeyParser(Namespaces.PHANTAZM); + EthyleneFeature.initialize(keyParser); + try { LOGGER.info("Loading server configuration data."); - ConfigFeature.initialize(); + ConfigFeature.initialize(EthyleneFeature.getMappingProcessorSource()); ConfigHandler handler = ConfigFeature.getHandler(); handler.writeDefaultsAndGet(); @@ -127,9 +140,15 @@ else if (serverInfoConfig.isUnsafeConfiguration()) { return; } + playerConfig = handler.loadDataNow(ConfigFeature.PLAYER_CONFIG_KEY); lobbiesConfig = handler.loadDataNow(ConfigFeature.LOBBIES_CONFIG_KEY); pathfinderConfig = handler.loadDataNow(ConfigFeature.PATHFINDER_CONFIG_KEY); shutdownConfig = handler.loadDataNow(ConfigFeature.SHUTDOWN_CONFIG_KEY); + startupConfig = handler.loadDataNow(ConfigFeature.STARTUP_CONFIG_KEY); + partyConfig = handler.loadDataNow(ConfigFeature.PARTY_CONFIG_KEY); + whisperConfig = handler.loadDataNow(ConfigFeature.WHISPER_CONFIG_KEY); + chatConfig = handler.loadDataNow(ConfigFeature.CHAT_CONFIG_KEY); + zombiesConfig = handler.loadDataNow(ConfigFeature.ZOMBIES_CONFIG_KEY); LOGGER.info("Server configuration loaded successfully."); } catch (ConfigProcessException e) { @@ -144,7 +163,8 @@ else if (serverInfoConfig.isUnsafeConfiguration()) { EventNode node = MinecraftServer.getGlobalEventHandler(); try { LOGGER.info("Initializing features."); - initializeFeatures(node, serverConfig, shutdownConfig, pathfinderConfig, lobbiesConfig, loginValidator); + initializeFeatures(keyParser, node, playerConfig, serverConfig, shutdownConfig, pathfinderConfig, + lobbiesConfig, partyConfig, whisperConfig, chatConfig, zombiesConfig, loginValidator); LOGGER.info("Features initialized successfully."); } catch (Exception exception) { @@ -160,7 +180,7 @@ else if (serverInfoConfig.isUnsafeConfiguration()) { MinecraftServer.setBrandName(BRAND_NAME); try { - startServer(node, minecraftServer, serverConfig); + startServer(node, minecraftServer, serverConfig, startupConfig); } catch (Exception exception) { LOGGER.error("Fatal error during server startup", exception); @@ -171,9 +191,12 @@ else if (serverInfoConfig.isUnsafeConfiguration()) { public static void shutdown(@Nullable String reason) { LOGGER.info("Shutting down server. Reason: " + reason); - ZombiesFeature.end(); - loginValidator.flush(); + HikariFeature.end(); + if (loginValidator != null) { + loginValidator.flush(); + } + ExecutorFeature.shutdown(); MinecraftServer.stopCleanly(); ServerCommandFeature.flushPermissions(); } @@ -188,23 +211,27 @@ private static boolean isUnsafe(String[] args) { return false; } - private static void initializeFeatures(EventNode global, ServerConfig serverConfig, - ShutdownConfig shutdownConfig, PathfinderConfig pathfinderConfig, LobbiesConfig lobbiesConfig, - LoginValidator loginValidator) throws Exception { + private static void initializeFeatures(KeyParser keyParser, EventNode global, PlayerConfig playerConfig, + ServerConfig serverConfig, ShutdownConfig shutdownConfig, PathfinderConfig pathfinderConfig, + LobbiesConfig lobbiesConfig, PartyConfig partyConfig, WhisperConfig whisperConfig, ChatConfig chatConfig, + ZombiesConfig zombiesConfig, LoginValidator loginValidator) throws Exception { + DatapackFeature.initialize(MinecraftServer.getBiomeManager()); + PlayerFeature.initialize(playerConfig, global, MiniMessage.miniMessage()); + RouterStore routerStore = new BasicRouterStore(); + ExecutorFeature.initialize(); + HikariFeature.initialize(); + GeneralStatsFeature.initialize(global); BlockHandlerFeature.initialize(MinecraftServer.getBlockManager()); - KeyParser keyParser = new BasicKeyParser(Namespaces.PHANTAZM); - SongFeature.initialize(keyParser); - EthyleneFeature.initialize(keyParser); MappingProcessorSource mappingProcessorSource = EthyleneFeature.getMappingProcessorSource(); ElementFeature.initialize(mappingProcessorSource, keyParser); ConfigCodec tomlCodec = new TomlCodec(); WhisperCommandFeature.initialize(MinecraftServer.getCommandManager(), MinecraftServer.getConnectionManager(), - mappingProcessorSource, tomlCodec); + whisperConfig); ContextManager contextManager = ElementFeature.getContextManager(); @@ -214,15 +241,15 @@ private static void initializeFeatures(EventNode global, ServerConfig ser PlayerViewProvider viewProvider = new BasicPlayerViewProvider(IdentitySource.MOJANG, MinecraftServer.getConnectionManager()); - CommandFeature.initialize(MinecraftServer.getCommandManager(), routerStore, viewProvider); PartyFeature.initialize(MinecraftServer.getCommandManager(), viewProvider, - MinecraftServer.getSchedulerManager(), mappingProcessorSource, contextManager, tomlCodec, + MinecraftServer.getSchedulerManager(), contextManager, partyConfig, tomlCodec, MiniMessage.miniMessage()); LobbyFeature.initialize(global, viewProvider, lobbiesConfig, contextManager); - ChatFeature.initialize(global, viewProvider, PartyFeature.getPartyHolder().uuidToGuild(), + ChatFeature.initialize(global, viewProvider, chatConfig, PartyFeature.getPartyHolder().uuidToGuild(), MinecraftServer.getCommandManager()); - MessagingFeature.initialize(global, serverConfig.serverInfoConfig().authType()); + CommandFeature.initialize(MinecraftServer.getCommandManager(), routerStore, viewProvider, + LobbyFeature.getFallback()); ProximaFeature.initialize(global, contextManager, pathfinderConfig); @@ -234,27 +261,27 @@ private static void initializeFeatures(EventNode global, ServerConfig ser mappingProcessorSource.processorFor(Token.ofClass(EquipmentData.class))); CommandManager commandManager = MinecraftServer.getCommandManager(); + + SceneTransferHelper transferHelper = new SceneTransferHelper(routerStore); ZombiesFeature.initialize(global, contextManager, MobFeature.getProcessorMap(), ProximaFeature.getSpawner(), keyParser, ProximaFeature.instanceSettingsFunction(), viewProvider, commandManager, new CompositeFallback(List.of(LobbyFeature.getFallback(), new KickFallback(Component.text("Failed to send you to lobby", NamedTextColor.RED)))), - PartyFeature.getPartyHolder().uuidToGuild(), routerStore); + PartyFeature.getPartyHolder().uuidToGuild(), transferHelper, SongFeature.songLoader(), zombiesConfig); ServerCommandFeature.initialize(commandManager, loginValidator, serverConfig.serverInfoConfig().whitelist(), - mappingProcessorSource, codec, routerStore, shutdownConfig); + mappingProcessorSource, codec, routerStore, shutdownConfig, zombiesConfig.gamereportConfig(), + viewProvider, transferHelper); ValidationFeature.initialize(global, loginValidator, ServerCommandFeature.permissionHandler()); routerStore.putRouter(RouterKeys.ZOMBIES_SCENE_ROUTER, ZombiesFeature.zombiesSceneRouter()); routerStore.putRouter(RouterKeys.LOBBY_SCENE_ROUTER, LobbyFeature.getLobbyRouter()); } - private static void startServer(EventNode node, MinecraftServer server, ServerConfig serverConfig) { + private static void startServer(EventNode node, MinecraftServer server, ServerConfig serverConfig, + StartupConfig startupConfig) { ServerInfoConfig infoConfig = serverConfig.serverInfoConfig(); - if (infoConfig.optifineEnabled()) { - OptifineSupport.enable(); - } - switch (infoConfig.authType()) { case MOJANG -> MojangAuth.init(); case BUNGEE -> { @@ -268,6 +295,17 @@ private static void startServer(EventNode node, MinecraftServer server, S event -> event.getResponseData().setDescription(serverConfig.pingListConfig().description())); server.start(infoConfig.serverIP(), infoConfig.port()); + + if (startupConfig.hasCommand()) { + ProcessBuilder processBuilder = new ProcessBuilder(startupConfig.command()); + try { + processBuilder.start(); + } + catch (IOException e) { + LOGGER.warn("Failed to run startup command", e); + } + } + LOGGER.info("serverIP: " + infoConfig.serverIP() + ", port: " + infoConfig.port()); } } diff --git a/server/src/main/java/org/phantazm/server/PlayerFeature.java b/server/src/main/java/org/phantazm/server/PlayerFeature.java new file mode 100644 index 000000000..050ac99b5 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/PlayerFeature.java @@ -0,0 +1,36 @@ +package org.phantazm.server; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.player.PlayerSpawnEvent; +import org.jetbrains.annotations.NotNull; +import org.phantazm.server.config.player.PlayerConfig; + +public final class PlayerFeature { + + private PlayerFeature() { + throw new UnsupportedOperationException(); + } + + static void initialize(@NotNull PlayerConfig playerConfig, @NotNull EventNode global, + @NotNull MiniMessage miniMessage) { + global.addListener(PlayerSpawnEvent.class, event -> { + if (!event.isFirstSpawn()) { + return; + } + + TagResolver namePlaceholder = Placeholder.component("name", event.getPlayer().getName()); + Component newName = miniMessage.deserialize(playerConfig.nameFormat(), namePlaceholder); + event.getPlayer().setDisplayName(newName); + event.getPlayer().setCustomName(newName); + event.getPlayer().setCustomNameVisible(true); + + event.getPlayer().sendMessage(playerConfig.joinMessage()); + }); + } + +} diff --git a/server/src/main/java/org/phantazm/server/ServerCommandFeature.java b/server/src/main/java/org/phantazm/server/ServerCommandFeature.java index b285a7c61..16b45bce6 100644 --- a/server/src/main/java/org/phantazm/server/ServerCommandFeature.java +++ b/server/src/main/java/org/phantazm/server/ServerCommandFeature.java @@ -7,9 +7,12 @@ import net.minestom.server.permission.Permission; import org.jetbrains.annotations.NotNull; import org.phantazm.core.game.scene.RouterStore; +import org.phantazm.core.game.scene.SceneTransferHelper; import org.phantazm.core.player.IdentitySource; +import org.phantazm.core.player.PlayerViewProvider; import org.phantazm.server.command.server.*; import org.phantazm.server.config.server.ShutdownConfig; +import org.phantazm.server.config.server.ZombiesGamereportConfig; import org.phantazm.server.permission.FilePermissionHandler; import org.phantazm.server.permission.PermissionHandler; import org.phantazm.server.player.LoginValidator; @@ -25,7 +28,8 @@ private ServerCommandFeature() { static void initialize(@NotNull CommandManager commandManager, @NotNull LoginValidator loginValidator, boolean whitelist, @NotNull MappingProcessorSource mappingProcessorSource, @NotNull ConfigCodec permissionsCodec, @NotNull RouterStore routerStore, - @NotNull ShutdownConfig shutdownConfig) { + @NotNull ShutdownConfig shutdownConfig, @NotNull ZombiesGamereportConfig zombiesGamereportConfig, + @NotNull PlayerViewProvider playerViewProvider, @NotNull SceneTransferHelper sceneTransferHelper) { ServerCommandFeature.permissionHandler = new FilePermissionHandler(mappingProcessorSource, permissionsCodec, PhantazmServer.PERMISSIONS_FILE); @@ -37,6 +41,8 @@ static void initialize(@NotNull CommandManager commandManager, @NotNull LoginVal commandManager.register( new OrderlyShutdownCommand(routerStore, shutdownConfig, MinecraftServer.getGlobalEventHandler())); commandManager.register(new DebugCommand()); + commandManager.register(new GamereportCommand(routerStore, zombiesGamereportConfig)); + commandManager.register(new GhostCommand(playerViewProvider, sceneTransferHelper, routerStore)); commandManager.getConsoleSender().addPermission(ALL_PERMISSIONS); } diff --git a/server/src/main/java/org/phantazm/server/WhisperCommandFeature.java b/server/src/main/java/org/phantazm/server/WhisperCommandFeature.java index 6fb937fb7..a29b1b0a5 100644 --- a/server/src/main/java/org/phantazm/server/WhisperCommandFeature.java +++ b/server/src/main/java/org/phantazm/server/WhisperCommandFeature.java @@ -1,11 +1,5 @@ package org.phantazm.server; -import com.github.steanky.ethylene.codec.toml.TomlCodec; -import com.github.steanky.ethylene.core.ConfigCodec; -import com.github.steanky.ethylene.core.bridge.Configuration; -import com.github.steanky.ethylene.core.processor.ConfigProcessor; -import com.github.steanky.ethylene.mapper.MappingProcessorSource; -import com.github.steanky.ethylene.mapper.type.Token; import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.command.CommandManager; import net.minestom.server.network.ConnectionManager; @@ -16,20 +10,10 @@ import org.phantazm.server.command.whisper.WhisperManager; import java.io.IOException; -import java.nio.file.Path; public class WhisperCommandFeature { static void initialize(@NotNull CommandManager commandManager, @NotNull ConnectionManager connectionManager, - @NotNull MappingProcessorSource mappingProcessorSource, @NotNull ConfigCodec whisperCodec) - throws IOException { - ConfigProcessor whisperConfigProcessor = - mappingProcessorSource.processorFor(Token.ofClass(WhisperConfig.class)); - String whisperFileName = whisperCodec.getPreferredExtensions().isEmpty() - ? "whisper" - : "whisper." + whisperCodec.getPreferredExtension(); - WhisperConfig whisperConfig = - Configuration.read(Path.of(whisperFileName), whisperCodec, - whisperConfigProcessor); + @NotNull WhisperConfig whisperConfig) throws IOException { WhisperManager whisperManager = new WhisperManager(connectionManager, commandManager.getConsoleSender(), whisperConfig, MiniMessage.miniMessage()); diff --git a/server/src/main/java/org/phantazm/server/ZombiesFeature.java b/server/src/main/java/org/phantazm/server/ZombiesFeature.java index bfe8bc723..e7e42082a 100644 --- a/server/src/main/java/org/phantazm/server/ZombiesFeature.java +++ b/server/src/main/java/org/phantazm/server/ZombiesFeature.java @@ -5,30 +5,24 @@ import com.github.steanky.ethylene.codec.yaml.YamlCodec; import com.github.steanky.ethylene.core.ConfigCodec; import com.github.steanky.ethylene.core.processor.ConfigProcessor; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import it.unimi.dsi.fastutil.booleans.BooleanObjectPair; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandManager; +import net.minestom.server.coordinate.Point; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.instance.DynamicChunk; import net.minestom.server.instance.Instance; -import net.minestom.server.network.packet.server.play.TeamsPacket; -import net.minestom.server.scoreboard.Team; -import net.minestom.server.scoreboard.TeamManager; import net.minestom.server.timer.TaskSchedule; -import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.phantazm.core.BasicClientBlockHandlerSource; -import org.phantazm.core.InstanceClientBlockHandler; +import org.phantazm.core.ClientBlockHandlerSource; import org.phantazm.core.VecUtils; import org.phantazm.core.equipment.LinearUpgradePath; import org.phantazm.core.equipment.NoUpgradePath; -import org.phantazm.core.game.scene.RouterStore; import org.phantazm.core.game.scene.SceneTransferHelper; import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.guild.party.Party; @@ -39,12 +33,18 @@ import org.phantazm.core.particle.ParticleWrapper; import org.phantazm.core.particle.data.*; import org.phantazm.core.player.PlayerViewProvider; +import org.phantazm.core.sound.SongLoader; import org.phantazm.proxima.bindings.minestom.InstanceSpawner; import org.phantazm.proxima.bindings.minestom.Spawner; -import org.phantazm.stats.zombies.*; +import org.phantazm.server.config.zombies.ZombiesConfig; +import org.phantazm.stats.zombies.JooqZombiesSQLFetcher; +import org.phantazm.stats.zombies.SQLZombiesDatabase; +import org.phantazm.stats.zombies.ZombiesDatabase; +import org.phantazm.stats.zombies.ZombiesSQLFetcher; import org.phantazm.zombies.Attributes; import org.phantazm.zombies.command.ZombiesCommand; import org.phantazm.zombies.corpse.CorpseCreator; +import org.phantazm.zombies.leaderboard.BestTimeLeaderboard; import org.phantazm.zombies.map.FileSystemMapLoader; import org.phantazm.zombies.map.Loader; import org.phantazm.zombies.map.MapInfo; @@ -65,7 +65,6 @@ import org.phantazm.zombies.map.shop.predicate.logic.AndPredicate; import org.phantazm.zombies.map.shop.predicate.logic.NotPredicate; import org.phantazm.zombies.map.shop.predicate.logic.OrPredicate; -import org.phantazm.zombies.map.shop.predicate.logic.XorPredicate; import org.phantazm.zombies.mob.BasicMobSpawnerSource; import org.phantazm.zombies.mob.MobSpawnerSource; import org.phantazm.zombies.player.BasicZombiesPlayerSource; @@ -76,7 +75,6 @@ import org.phantazm.zombies.powerup.action.*; import org.phantazm.zombies.powerup.predicate.ImmediateDeactivationPredicate; import org.phantazm.zombies.powerup.predicate.TimedDeactivationPredicate; -import org.phantazm.zombies.powerup.action.BossBarTimerAction; import org.phantazm.zombies.powerup.visual.HologramVisual; import org.phantazm.zombies.powerup.visual.ItemVisual; import org.phantazm.zombies.scene.ZombiesScene; @@ -98,9 +96,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; public final class ZombiesFeature { @@ -114,17 +110,21 @@ public final class ZombiesFeature { private static Map powerups; private static MobSpawnerSource mobSpawnerSource; private static ZombiesSceneRouter sceneRouter; - private static ExecutorService databaseExecutor; - private static HikariDataSource dataSource; private static ZombiesDatabase database; + private record PreloadedMap(@NotNull List instancePath, @NotNull Point spawn, int chunkLoadRange) { + + } + + static void initialize(@NotNull EventNode globalEventNode, @NotNull ContextManager contextManager, @NotNull Map, ConfigProcessor> processorMap, @NotNull Spawner spawner, @NotNull KeyParser keyParser, @NotNull Function instanceSpaceFunction, @NotNull PlayerViewProvider viewProvider, @NotNull CommandManager commandManager, @NotNull SceneFallback sceneFallback, @NotNull Map parties, - @NotNull RouterStore routerStore) throws IOException { + @NotNull SceneTransferHelper sceneTransferHelper, @NotNull SongLoader songLoader, + @NotNull ZombiesConfig zombiesConfig) throws IOException { Attributes.registerAll(); registerElementClasses(contextManager); @@ -135,56 +135,54 @@ static void initialize(@NotNull EventNode globalEventNode, @NotNull Conte InstanceLoader instanceLoader = new AnvilFileSystemInstanceLoader(MinecraftServer.getInstanceManager(), INSTANCES_FOLDER, - DynamicChunk::new); + DynamicChunk::new, ExecutorFeature.getExecutor()); + + Set instancePaths = new HashSet<>(maps.size()); + for (MapInfo mapInfo : maps.values()) { + instancePaths.add(new PreloadedMap(mapInfo.settings().instancePath(), + VecUtils.toPoint(mapInfo.settings().origin().add(mapInfo.settings().spawn())), + mapInfo.settings().chunkLoadRange())); + } - LOGGER.info("Preloading {} map instances", maps.size()); - for (MapInfo map : maps.values()) { - instanceLoader.preload(map.settings().instancePath(), - VecUtils.toPoint(map.settings().origin().add(map.settings().spawn())), - map.settings().chunkLoadRange()); + LOGGER.info("Preloading {} map instances", instancePaths.size()); + List> loadFutures = new ArrayList<>(instancePaths.size()); + for (PreloadedMap map : instancePaths) { + loadFutures.add(CompletableFuture.runAsync(() -> { + instanceLoader.preload(map.instancePath, map.spawn, map.chunkLoadRange); + })); } + CompletableFuture.allOf(loadFutures.toArray(CompletableFuture[]::new)).join(); + Map providers = new HashMap<>(maps.size()); - TeamManager teamManager = MinecraftServer.getTeamManager(); - - // https://bugs.mojang.com/browse/MC-87984 - Team mobNoPushTeam = - teamManager.createBuilder("mobNoPush").collisionRule(TeamsPacket.CollisionRule.PUSH_OTHER_TEAMS) - .build(); - Team corpseTeam = teamManager.createBuilder("corpses").collisionRule(TeamsPacket.CollisionRule.NEVER) - .nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER).build(); - - databaseExecutor = Executors.newSingleThreadExecutor(); - HikariConfig config = new HikariConfig("./zombies.hikari.properties"); - dataSource = new HikariDataSource(config); + ZombiesSQLFetcher sqlFetcher = new JooqZombiesSQLFetcher(); - database = new SQLZombiesDatabase(databaseExecutor, dataSource, sqlFetcher); + database = new SQLZombiesDatabase(ExecutorFeature.getExecutor(), HikariFeature.getDataSource(), sqlFetcher); + + ClientBlockHandlerSource clientBlockHandlerSource = new BasicClientBlockHandlerSource(globalEventNode); for (Map.Entry entry : maps.entrySet()) { ZombiesSceneProvider provider = - new ZombiesSceneProvider(20, instanceSpaceFunction, entry.getValue(), instanceLoader, sceneFallback, - globalEventNode, ZombiesFeature.mobSpawnerSource(), MobFeature.getModels(), - new BasicClientBlockHandlerSource(instance -> { - DimensionType dimensionType = instance.getDimensionType(); - return new InstanceClientBlockHandler(instance, dimensionType.getMinY(), - dimensionType.getHeight()); - }, globalEventNode), contextManager, keyParser, mobNoPushTeam, corpseTeam, database, - ZombiesFeature.powerups(), - new BasicZombiesPlayerSource(EquipmentFeature::createEquipmentCreator, - MobFeature.getModels()), + new ZombiesSceneProvider(ExecutorFeature.getExecutor(), zombiesConfig.maximumScenes(), + instanceSpaceFunction, entry.getValue(), instanceLoader, sceneFallback, globalEventNode, + ZombiesFeature.mobSpawnerSource(), MobFeature.getModels(), clientBlockHandlerSource, + contextManager, keyParser, database, ZombiesFeature.powerups(), + new BasicZombiesPlayerSource(database, viewProvider, + EquipmentFeature::createEquipmentCreator, MobFeature.getModels(), contextManager, + keyParser), mapDependencyProvider -> contextManager.makeContext(entry.getValue().corpse()) - .provide(mapDependencyProvider)); + .provide(mapDependencyProvider), songLoader); providers.put(entry.getKey(), provider); } ZombiesFeature.sceneRouter = new ZombiesSceneRouter(providers); - MinecraftServer.getSchedulerManager() - .scheduleTask(() -> sceneRouter.tick(System.currentTimeMillis()), TaskSchedule.immediate(), - TaskSchedule.nextTick()); + MinecraftServer.getSchedulerManager().scheduleTask(() -> { + sceneRouter.tick(System.currentTimeMillis()); + }, TaskSchedule.immediate(), TaskSchedule.nextTick()); - SceneTransferHelper transferHelper = new SceneTransferHelper(routerStore); commandManager.register( - new ZombiesCommand(parties, sceneRouter, keyParser, maps, viewProvider, transferHelper, sceneFallback)); + new ZombiesCommand(parties, sceneRouter, keyParser, maps, viewProvider, sceneTransferHelper, + sceneFallback)); } private static void registerElementClasses(ContextManager contextManager) { @@ -213,28 +211,45 @@ private static void registerElementClasses(ContextManager contextManager) { contextManager.registerElementClass(AndPredicate.class); contextManager.registerElementClass(OrPredicate.class); contextManager.registerElementClass(NotPredicate.class); - contextManager.registerElementClass(XorPredicate.class); contextManager.registerElementClass(EquipmentCostPredicate.class); contextManager.registerElementClass(EquipmentSpacePredicate.class); contextManager.registerElementClass(EquipmentPresentPredicate.class); + contextManager.registerElementClass(EquipmentTierPredicate.class); //ShopInteractor contextManager.registerElementClass(MapFlaggingInteractor.class); contextManager.registerElementClass(PlayerFlaggingInteractor.class); contextManager.registerElementClass(MessagingInteractor.class); contextManager.registerElementClass(PlaySoundInteractor.class); + contextManager.registerElementClass(RefillAllAmmoInteractor.class); contextManager.registerElementClass(DelayedInteractor.class); + contextManager.registerElementClass(DragonsWrathInteractor.class); contextManager.registerElementClass(ConditionalInteractor.class); + contextManager.registerElementClass(CountingInteractor.class); contextManager.registerElementClass(OpenGuiInteractor.class); contextManager.registerElementClass(CloseGuiInteractor.class); + contextManager.registerElementClass(CostSubstitutingItem.class); contextManager.registerElementClass(AddEquipmentInteractor.class); + contextManager.registerElementClass(EquipmentStationInteractor.class); contextManager.registerElementClass(ChangeDoorStateInteractor.class); contextManager.registerElementClass(DeductCoinsInteractor.class); contextManager.registerElementClass(RefillAmmoInteractor.class); + contextManager.registerElementClass(ReviveAllInteractor.class); contextManager.registerElementClass(UpdateBlockPropertiesInteractor.class); contextManager.registerElementClass(SubstitutedTitleInteractor.class); contextManager.registerElementClass(TitleInteractor.class); + contextManager.registerElementClass(SlotMachineInteractor.class); + contextManager.registerElementClass(SlotMachineInteractor.BasicSlotMachineFrame.class); + contextManager.registerElementClass(SlotMachineInteractor.ConstantDelayFormula.class); + contextManager.registerElementClass(SlotMachineInteractor.LinearDelayFormula.class); + contextManager.registerElementClass(PlaySongInteractor.class); + contextManager.registerElementClass(ChangeSelectionGroupInteractor.class); + contextManager.registerElementClass(SelectionGroupInteractor.class); + contextManager.registerElementClass(AnnounceActiveSelectionGroup.class); + contextManager.registerElementClass(AdditiveTransactionModifierInteractor.class); + contextManager.registerElementClass(UpgradeEquipmentInteractor.class); + contextManager.registerElementClass(ChestStateInteractor.class); //ShopDisplay contextManager.registerElementClass(StaticHologramDisplay.class); @@ -253,6 +268,8 @@ private static void registerElementClasses(ContextManager contextManager) { contextManager.registerElementClass(InteractionPoint.class); contextManager.registerElementClass(CombiningArmorPlayerDisplayCreator.class); + contextManager.registerElementClass(BestTimeLeaderboard.class); + //Sidebar contextManager.registerElementClass(SidebarUpdater.class); contextManager.registerElementClass(CollectionSidebarSection.class); @@ -263,7 +280,6 @@ private static void registerElementClasses(ContextManager contextManager) { contextManager.registerElementClass(ConstantSidebarLineUpdater.class); contextManager.registerElementClass(DateLineUpdater.class); contextManager.registerElementClass(JoinedPlayersSidebarLineUpdater.class); - contextManager.registerElementClass(PlayerStateSidebarLineUpdater.class); contextManager.registerElementClass(RemainingZombiesSidebarLineUpdater.class); contextManager.registerElementClass(RoundSidebarLineUpdater.class); contextManager.registerElementClass(TicksLineUpdater.class); @@ -318,6 +334,7 @@ private static void registerElementClasses(ContextManager contextManager) { contextManager.registerElementClass(SendMessageAction.class); contextManager.registerElementClass(AttributeModifierAction.class); contextManager.registerElementClass(PreventAmmoDrainAction.class); + contextManager.registerElementClass(RefillAmmoAction.class); //Player conditions contextManager.registerElementClass(EquipmentCondition.class); @@ -384,31 +401,4 @@ private static Map loadFeature(String featureName, Loa return FeatureUtils.check(database); } - public static void end() { - if (dataSource != null) { - dataSource.close(); - } - - if (databaseExecutor != null) { - databaseExecutor.shutdown(); - - try { - LOGGER.info( - "Shutting down database executor. Please allow for one minute before shutdown " + "completes."); - if (!databaseExecutor.awaitTermination(1L, TimeUnit.MINUTES)) { - databaseExecutor.shutdownNow(); - - LOGGER.warn( - "Not all database tasks completed. Please allow for one minute for tasks to be canceled."); - if (!databaseExecutor.awaitTermination(1L, TimeUnit.MINUTES)) { - LOGGER.warn("Database tasks failed to cancel."); - } - } - } - catch (InterruptedException e) { - databaseExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } } diff --git a/server/src/main/java/org/phantazm/server/command/server/GamereportCommand.java b/server/src/main/java/org/phantazm/server/command/server/GamereportCommand.java new file mode 100644 index 000000000..b5bb2a9ad --- /dev/null +++ b/server/src/main/java/org/phantazm/server/command/server/GamereportCommand.java @@ -0,0 +1,241 @@ +package org.phantazm.server.command.server; + +import com.github.steanky.element.core.key.Constants; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.permission.Permission; +import org.intellij.lang.annotations.Subst; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.game.scene.RouterStore; +import org.phantazm.core.game.scene.Scene; +import org.phantazm.core.game.scene.SceneRouter; +import org.phantazm.core.time.AnalogTickFormatter; +import org.phantazm.core.time.TickFormatter; +import org.phantazm.server.config.server.ZombiesGamereportConfig; +import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.scene.ZombiesScene; +import org.phantazm.zombies.scene.ZombiesSceneRouter; +import org.phantazm.zombies.stage.EndStage; +import org.phantazm.zombies.stage.InGameStage; +import org.phantazm.zombies.stage.Stage; +import org.phantazm.zombies.stage.StageKeys; + +import java.time.Instant; +import java.util.*; + +public class GamereportCommand extends Command { + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private interface PageFormatter { + @NotNull Component page(int pageIndex, @NotNull List> scenes); + + int itemsPerPage(); + } + + private record ZombiesPageFormatter(ZombiesGamereportConfig config) implements PageFormatter { + private static final int ITEMS_PER_PAGE = 3; + private static final TickFormatter TIME_FORMATTER = new AnalogTickFormatter(new AnalogTickFormatter.Data(true)); + + @Override + public @NotNull Component page(int page, @NotNull List> scenes) { + int maxPages = (int)Math.ceil(scenes.size() / (double)ITEMS_PER_PAGE); + + List gameEntries = new ArrayList<>(scenes.size()); + + TagResolver totalGamesTag = Placeholder.unparsed("total_games", Integer.toString(scenes.size())); + + for (int i = (page - 1) * ITEMS_PER_PAGE, j = 0; i < scenes.size() && j < ITEMS_PER_PAGE; i++, j++) { + ZombiesScene zombiesScene = (ZombiesScene)scenes.get(i); + + TagResolver currentGameTag = Placeholder.unparsed("current_game", Integer.toString(i + 1)); + TagResolver gameUUIDTag = Placeholder.unparsed("game_uuid", zombiesScene.getUUID().toString()); + + List playerNames = new ArrayList<>(zombiesScene.getMapSettingsInfo().maxPlayers()); + for (ZombiesPlayer player : zombiesScene.getZombiesPlayers().values()) { + player.getPlayer().ifPresent(actualPlayer -> { + Component displayName = actualPlayer.getDisplayName(); + playerNames.add(displayName == null ? Component.text(actualPlayer.getUsername()) : displayName); + }); + } + + Component playerList = Component.join(JoinConfiguration.commas(true), playerNames); + TagResolver playerListTag = Placeholder.component("player_list", playerList); + TagResolver mapNameTag = + Placeholder.component("map_name", zombiesScene.getMapSettingsInfo().displayName()); + + Component gameState; + Stage currentStage = zombiesScene.getCurrentStage(); + + TagResolver currentRoundTag = Placeholder.parsed("current_round", + Integer.toString(zombiesScene.getMap().roundHandler().currentRoundIndex() + 1)); + + if (currentStage == null || currentStage.key().equals(StageKeys.IDLE_STAGE)) { + gameState = MINI_MESSAGE.deserialize(config.idleStageFormat()); + } + else if (currentStage.key().equals(StageKeys.COUNTDOWN)) { + gameState = MINI_MESSAGE.deserialize(config.countdownStageFormat()); + } + else if (currentStage.key().equals(StageKeys.IN_GAME)) { + InGameStage inGameStage = (InGameStage)currentStage; + + TagResolver gameTimeTag = + Placeholder.unparsed("game_time", TIME_FORMATTER.format(inGameStage.ticksSinceStart())); + + gameState = MINI_MESSAGE.deserialize(config.inGameFormat(), currentRoundTag, gameTimeTag); + } + else if (currentStage.key().equals(StageKeys.END)) { + EndStage endStage = (EndStage)currentStage; + + TagResolver gameTimeTag = + Placeholder.parsed("game_time", TIME_FORMATTER.format(endStage.ticksSinceStart())); + gameState = MINI_MESSAGE.deserialize(config.endedFormat(), + Formatter.choice("result", endStage.hasWon() ? 1 : 0), currentRoundTag, gameTimeTag); + } + else { + gameState = Component.empty(); + } + + TagResolver gameStateTag = Placeholder.component("game_state", gameState); + + TagResolver warpTag = TagResolver.resolver("warp", + Tag.styling(ClickEvent.runCommand("/ghost " + zombiesScene.getUUID()))); + + gameEntries.add( + MINI_MESSAGE.deserialize(config.gameEntryFormat(), totalGamesTag, currentGameTag, gameUUIDTag, + playerListTag, mapNameTag, gameStateTag, warpTag)); + } + + Component gameList = Component.join(JoinConfiguration.newlines(), gameEntries); + + TagResolver currentPageTag = Placeholder.unparsed("current_page", Integer.toString(page)); + TagResolver maxPagesTag = Placeholder.unparsed("max_pages", Integer.toString(maxPages)); + TagResolver timeTag = Placeholder.unparsed("current_time", Instant.now().toString()); + TagResolver gameListTag = Placeholder.component("game_list", gameList); + + Component nextPageOptionalComponent; + if (page * ITEMS_PER_PAGE >= scenes.size()) { + nextPageOptionalComponent = Component.empty(); + } + else { + TagResolver pageAdvancingResolver = TagResolver.resolver("next_page", + Tag.styling(ClickEvent.runCommand("/gamereport phantazm:zombies " + (page + 1)))); + + nextPageOptionalComponent = MINI_MESSAGE.deserialize(config.nextPageFormat(), pageAdvancingResolver); + } + + Component previousPageOptionalComponent; + if (page == 1) { + previousPageOptionalComponent = Component.empty(); + } + else { + TagResolver pageRetractingResolver = TagResolver.resolver("previous_page", + Tag.styling(ClickEvent.runCommand("/gamereport phantazm:zombies " + (page - 1)))); + + previousPageOptionalComponent = + MINI_MESSAGE.deserialize(config.previousPageFormat(), pageRetractingResolver); + } + + TagResolver nextPageOptionalTag = Placeholder.component("next_page_optional", nextPageOptionalComponent); + TagResolver previousPageOptionalTag = + Placeholder.component("previous_page_optional", previousPageOptionalComponent); + + return MINI_MESSAGE.deserialize(config.pageFormat(), currentPageTag, maxPagesTag, timeTag, totalGamesTag, + gameListTag, nextPageOptionalTag, previousPageOptionalTag); + } + + @Override + public int itemsPerPage() { + return ITEMS_PER_PAGE; + } + } + + public static final Permission PERMISSION = new Permission("admin.gamereport"); + + private final Map pageFormatters; + + public GamereportCommand(@NotNull RouterStore routerStore, @NotNull ZombiesGamereportConfig config) { + super("gamereport"); + + Map temp = new HashMap<>(); + temp.put(ZombiesSceneRouter.KEY, new ZombiesPageFormatter(config)); + pageFormatters = Map.copyOf(temp); + + Argument routerArgument = ArgumentType.String("router-key"); + routerArgument.setSuggestionCallback((sender, context, suggestion) -> { + for (SceneRouter router : routerStore.getRouters()) { + if (!router.isGame()) { + continue; + } + + suggestion.addEntry( + new SuggestionEntry(router.key().asString(), Component.text(router.key().asString()))); + } + }); + + Argument pageArgument = ArgumentType.Integer("page").setDefaultValue(1); + + setCondition((sender, commandString) -> sender.hasPermission(PERMISSION)); + + addConditionalSyntax(getCondition(), (sender, context) -> { + @Subst(Constants.NAMESPACE_OR_KEY) + String key = context.get(routerArgument); + + if (!Key.parseable(key)) { + sender.sendMessage(Component.text("Invalid key!").color(NamedTextColor.RED)); + return; + } + + Key routerKey = Key.key(key); + SceneRouter targetRouter = null; + for (SceneRouter router : routerStore.getRouters()) { + if (!router.isGame()) { + continue; + } + + if (router.key().equals(routerKey)) { + targetRouter = router; + break; + } + } + + if (targetRouter == null) { + sender.sendMessage(Component.text("Target router does not exist!").color(NamedTextColor.RED)); + return; + } + + PageFormatter formatter = pageFormatters.get(targetRouter.key()); + if (formatter == null) { + sender.sendMessage(Component.text("No formatter for this scene!").color(NamedTextColor.RED)); + return; + } + + List> scenes = List.copyOf(targetRouter.getScenes()); + + if (scenes.isEmpty()) { + sender.sendMessage(Component.text("There are no scenes in this router!").color(NamedTextColor.RED)); + return; + } + + int pageCount = (int)Math.ceil(scenes.size() / (double)formatter.itemsPerPage()); + int page = context.get(pageArgument); + if (page < 1 || page > pageCount) { + sender.sendMessage(Component.text("Target page does not exist!").color(NamedTextColor.RED)); + return; + } + + sender.sendMessage(formatter.page(page, scenes)); + }, routerArgument, pageArgument); + } +} diff --git a/server/src/main/java/org/phantazm/server/command/server/GhostCommand.java b/server/src/main/java/org/phantazm/server/command/server/GhostCommand.java new file mode 100644 index 000000000..29d98d860 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/command/server/GhostCommand.java @@ -0,0 +1,66 @@ +package org.phantazm.server.command.server; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.arguments.minecraft.ArgumentUUID; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.entity.Player; +import net.minestom.server.permission.Permission; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.game.scene.RouterStore; +import org.phantazm.core.game.scene.Scene; +import org.phantazm.core.game.scene.SceneRouter; +import org.phantazm.core.game.scene.SceneTransferHelper; +import org.phantazm.core.player.PlayerViewProvider; + +import java.util.UUID; + +public class GhostCommand extends Command { + public static final Permission PERMISSION = new Permission("admin.ghost"); + + public GhostCommand(@NotNull PlayerViewProvider viewProvider, @NotNull SceneTransferHelper transferHelper, + @NotNull RouterStore routerStore) { + super("ghost"); + + setCondition((sender, commandString) -> sender.hasPermission(PERMISSION)); + + ArgumentUUID sceneArgument = ArgumentType.UUID("scene"); + sceneArgument.setSuggestionCallback((sender, context, suggestion) -> { + for (SceneRouter router : routerStore.getRouters()) { + if (!router.isGame()) { + continue; + } + + for (Scene scene : router.getScenes()) { + suggestion.addEntry(new SuggestionEntry(scene.getUUID().toString(), null)); + } + } + }); + + addConditionalSyntax(getCondition(), ((sender, context) -> { + if (!(sender instanceof Player player)) { + sender.sendMessage( + Component.text("You must be a player to use this command!").color(NamedTextColor.RED)); + return; + } + + UUID argument = context.get(sceneArgument); + for (SceneRouter router : routerStore.getRouters()) { + if (!router.isGame()) { + continue; + } + + for (Scene scene : router.getScenes()) { + if (scene.getUUID().equals(argument)) { + transferHelper.ghost(scene, viewProvider.fromPlayer(player)); + return; + } + } + } + + sender.sendMessage(Component.text("No scene found with that UUID").color(NamedTextColor.RED)); + }), sceneArgument); + } +} diff --git a/server/src/main/java/org/phantazm/server/command/server/OrderlyShutdownCommand.java b/server/src/main/java/org/phantazm/server/command/server/OrderlyShutdownCommand.java index e120a3e8f..3d188d160 100644 --- a/server/src/main/java/org/phantazm/server/command/server/OrderlyShutdownCommand.java +++ b/server/src/main/java/org/phantazm/server/command/server/OrderlyShutdownCommand.java @@ -1,16 +1,23 @@ package org.phantazm.server.command.server; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.adventure.audience.Audiences; import net.minestom.server.command.builder.Command; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; +import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; import net.minestom.server.permission.Permission; import net.minestom.server.timer.TaskSchedule; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.event.PlayerJoinLobbyEvent; import org.phantazm.core.game.scene.RouterStore; import org.phantazm.core.game.scene.SceneRouter; import org.phantazm.core.game.scene.event.SceneShutdownEvent; +import org.phantazm.core.game.scene.lobby.Lobby; +import org.phantazm.core.player.PlayerView; +import org.phantazm.server.RouterKeys; import org.phantazm.server.config.server.ShutdownConfig; import java.util.Objects; @@ -34,6 +41,14 @@ public OrderlyShutdownCommand(@NotNull RouterStore routerStore, @NotNull Shutdow return; } + globalNode.addListener(SceneShutdownEvent.class, this::onSceneShutdown); + globalNode.addListener(AsyncPlayerPreLoginEvent.class, event -> { + event.getPlayer().kick(Component.text("Server is not joinable", NamedTextColor.RED)); + }); + globalNode.addListener(PlayerJoinLobbyEvent.class, event -> { + event.getPlayer().kick(Component.text("Routing to fresh instance...", NamedTextColor.RED)); + }); + initialized = true; shutdownStart = System.currentTimeMillis(); @@ -42,6 +57,13 @@ public OrderlyShutdownCommand(@NotNull RouterStore routerStore, @NotNull Shutdow router.setJoinable(false); } } + for (Lobby lobby : routerStore.getRouter(RouterKeys.LOBBY_SCENE_ROUTER).getScenes()) { + for (PlayerView playerView : lobby.getPlayers().values()) { + playerView.getPlayer().ifPresent(player -> { + player.kick(Component.text("Server is shutting down", NamedTextColor.RED)); + }); + } + } MinecraftServer.getSchedulerManager().scheduleTask(() -> { long elapsedMs = System.currentTimeMillis() - shutdownStart; @@ -65,8 +87,6 @@ public OrderlyShutdownCommand(@NotNull RouterStore routerStore, @NotNull Shutdow } }, TaskSchedule.immediate(), TaskSchedule.tick(20)); - globalNode.addListener(SceneShutdownEvent.class, this::onSceneShutdown); - if (noGamesActive()) { System.exit(0); } diff --git a/server/src/main/java/org/phantazm/server/command/whisper/WhisperConfig.java b/server/src/main/java/org/phantazm/server/command/whisper/WhisperConfig.java index f45863416..3521b1790 100644 --- a/server/src/main/java/org/phantazm/server/command/whisper/WhisperConfig.java +++ b/server/src/main/java/org/phantazm/server/command/whisper/WhisperConfig.java @@ -1,11 +1,17 @@ package org.phantazm.server.command.whisper; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import org.jetbrains.annotations.NotNull; -public record WhisperConfig(@NotNull String toTargetFormat, @NotNull String toSenderFormat, - @NotNull TextColor fallbackNameColor, @NotNull Component consoleName, +public record WhisperConfig(@NotNull String toTargetFormat, + @NotNull String toSenderFormat, + @NotNull TextColor fallbackNameColor, + @NotNull Component consoleName, @NotNull Component defaultName) { + public static final WhisperConfig DEFAULT = + new WhisperConfig("", "", NamedTextColor.WHITE, Component.empty(), Component.empty()); + } diff --git a/server/src/main/java/org/phantazm/server/config/loader/LobbiesConfigProcessor.java b/server/src/main/java/org/phantazm/server/config/loader/LobbiesConfigProcessor.java index 391892a88..f92973f64 100644 --- a/server/src/main/java/org/phantazm/server/config/loader/LobbiesConfigProcessor.java +++ b/server/src/main/java/org/phantazm/server/config/loader/LobbiesConfigProcessor.java @@ -9,18 +9,17 @@ import com.github.steanky.ethylene.core.processor.ConfigProcessor; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Pos; +import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.ConfigProcessors; import org.phantazm.core.config.InstanceConfig; +import org.phantazm.core.config.processor.ItemStackConfigProcessors; import org.phantazm.server.config.lobby.LobbiesConfig; import org.phantazm.server.config.lobby.LobbyConfig; import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * {@link ConfigProcessor} used for {@link LobbiesConfig}s. @@ -67,9 +66,16 @@ public class LobbiesConfigProcessor implements ConfigProcessor { int maxPlayers = lobby.getValue().getNumberOrThrow("maxPlayers").intValue(); int maxLobbies = lobby.getValue().getNumberOrThrow("maxLobbies").intValue(); + String lobbyJoinFormat = lobby.getValue().getStringOrThrow("lobbyJoinFormat"); + + Collection defaultItems = ItemStackConfigProcessors.snbt().collectionProcessor() + .dataFromElement(lobby.getValue().getElementOrThrow("defaultItems")); + ConfigList npcs = lobby.getValue().getListOrDefault(ConfigList::of, "npcs"); - lobbies.put(lobby.getKey(), new LobbyConfig(instanceConfig, lobbyPaths, maxPlayers, maxLobbies, npcs)); + lobbies.put(lobby.getKey(), + new LobbyConfig(instanceConfig, lobbyPaths, maxPlayers, maxLobbies, defaultItems, + lobbyJoinFormat, npcs)); } return new LobbiesConfig(instancesPath, kickMessage, mainLobbyName, lobbies); @@ -107,6 +113,9 @@ public class LobbiesConfigProcessor implements ConfigProcessor { lobbyNode.put("lobbyPaths", lobbyPathsList); lobbyNode.putNumber("maxPlayers", lobby.getValue().maxPlayers()); lobbyNode.putNumber("maxLobbies", lobby.getValue().maxLobbies()); + lobbyNode.put("defaultItems", ItemStackConfigProcessors.snbt().collectionProcessor() + .elementFromData(lobby.getValue().defaultItems())); + lobbyNode.putString("lobbyJoinFormat", lobby.getValue().lobbyJoinFormat()); lobbyNode.put("npcs", lobby.getValue().npcs()); lobbiesNode.put(lobby.getKey(), lobbyNode); diff --git a/server/src/main/java/org/phantazm/server/config/loader/ServerConfigProcessor.java b/server/src/main/java/org/phantazm/server/config/loader/ServerConfigProcessor.java index 485178263..33763dff8 100644 --- a/server/src/main/java/org/phantazm/server/config/loader/ServerConfigProcessor.java +++ b/server/src/main/java/org/phantazm/server/config/loader/ServerConfigProcessor.java @@ -29,7 +29,6 @@ public class ServerConfigProcessor implements ConfigProcessor { throw new ConfigProcessException("Invalid port: " + port + ", must be in range [0, 65535]"); } - boolean optifineEnabled = serverInfo.getBooleanOrThrow("optifineEnabled"); boolean whitelist = serverInfo.getBooleanOrThrow("whitelist"); AuthType authType = AuthType.getByName(serverInfo.getStringOrThrow("authType").toUpperCase(Locale.ENGLISH)) .orElseThrow(() -> new ConfigProcessException( @@ -37,7 +36,7 @@ public class ServerConfigProcessor implements ConfigProcessor { String proxySecret = serverInfo.getStringOrThrow("proxySecret"); ServerInfoConfig serverInfoConfig = - new ServerInfoConfig(serverAddress, port, optifineEnabled, whitelist, authType, proxySecret); + new ServerInfoConfig(serverAddress, port, whitelist, authType, proxySecret); ConfigNode pingList = element.getNodeOrThrow("pingList"); Component description = COMPONENT_PROCESSOR.dataFromElement(pingList.getElementOrThrow("description")); @@ -52,7 +51,6 @@ public class ServerConfigProcessor implements ConfigProcessor { ServerInfoConfig serverInfoConfig = serverConfig.serverInfoConfig(); serverInfo.putString("serverIP", serverInfoConfig.serverIP()); serverInfo.putNumber("port", serverInfoConfig.port()); - serverInfo.putBoolean("optifineEnabled", serverInfoConfig.optifineEnabled()); serverInfo.putString("authType", serverInfoConfig.authType().name()); serverInfo.putString("proxySecret", serverInfoConfig.proxySecret()); diff --git a/server/src/main/java/org/phantazm/server/config/lobby/LobbyConfig.java b/server/src/main/java/org/phantazm/server/config/lobby/LobbyConfig.java index 52fde72e8..73a028094 100644 --- a/server/src/main/java/org/phantazm/server/config/lobby/LobbyConfig.java +++ b/server/src/main/java/org/phantazm/server/config/lobby/LobbyConfig.java @@ -2,10 +2,12 @@ import com.github.steanky.ethylene.core.collection.ConfigList; import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.phantazm.core.config.InstanceConfig; import org.phantazm.core.game.scene.lobby.Lobby; +import java.util.Collection; import java.util.List; import java.util.Objects; @@ -21,6 +23,8 @@ public record LobbyConfig(@NotNull InstanceConfig instanceConfig, @NotNull List lobbyPaths, int maxPlayers, int maxLobbies, + @NotNull Collection defaultItems, + @NotNull String lobbyJoinFormat, @NotNull ConfigList npcs) { /** diff --git a/server/src/main/java/org/phantazm/server/config/player/PlayerConfig.java b/server/src/main/java/org/phantazm/server/config/player/PlayerConfig.java new file mode 100644 index 000000000..5e49146ff --- /dev/null +++ b/server/src/main/java/org/phantazm/server/config/player/PlayerConfig.java @@ -0,0 +1,10 @@ +package org.phantazm.server.config.player; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +public record PlayerConfig(@NotNull String nameFormat, @NotNull Component joinMessage) { + + public static final PlayerConfig DEFAULT = new PlayerConfig("", Component.empty()); + +} diff --git a/server/src/main/java/org/phantazm/server/config/server/ServerInfoConfig.java b/server/src/main/java/org/phantazm/server/config/server/ServerInfoConfig.java index 781112fe3..a1a8d732c 100644 --- a/server/src/main/java/org/phantazm/server/config/server/ServerInfoConfig.java +++ b/server/src/main/java/org/phantazm/server/config/server/ServerInfoConfig.java @@ -9,13 +9,11 @@ * * @param serverIP The IP to run the server on * @param port The port to run the server on - * @param optifineEnabled Whether optifine support is enabled * @param authType The type of authentication the server will use * @param proxySecret The secret used for authentication */ public record ServerInfoConfig(@NotNull String serverIP, int port, - boolean optifineEnabled, boolean whitelist, @NotNull AuthType authType, @NotNull String proxySecret) { @@ -29,11 +27,6 @@ public record ServerInfoConfig(@NotNull String serverIP, */ public static final int DEFAULT_PORT = 25565; - /** - * The default Optifine patch status. - */ - public static final boolean DEFAULT_OPTIFINE_ENABLED = true; - /** * Whether this server should whitelist players, by default. */ @@ -54,7 +47,7 @@ public record ServerInfoConfig(@NotNull String serverIP, * The default ServerInfoConfig instance. */ public static final ServerInfoConfig DEFAULT = - new ServerInfoConfig(DEFAULT_SERVER_ADDRESS, DEFAULT_PORT, DEFAULT_OPTIFINE_ENABLED, DEFAULT_WHITELIST, + new ServerInfoConfig(DEFAULT_SERVER_ADDRESS, DEFAULT_PORT, DEFAULT_WHITELIST, DEFAULT_AUTH_TYPE, DEFAULT_PROXY_SECRET); /** @@ -62,7 +55,6 @@ public record ServerInfoConfig(@NotNull String serverIP, * * @param serverIP The IP to run the server on * @param port The port to run the server on - * @param optifineEnabled Whether optifine support is enabled * @param authType The type of authentication the server will use * @param proxySecret The secret used for authentication */ diff --git a/server/src/main/java/org/phantazm/server/config/server/StartupConfig.java b/server/src/main/java/org/phantazm/server/config/server/StartupConfig.java new file mode 100644 index 000000000..c7ef939b2 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/config/server/StartupConfig.java @@ -0,0 +1,9 @@ +package org.phantazm.server.config.server; + +import org.jetbrains.annotations.NotNull; + +public record StartupConfig(boolean hasCommand, @NotNull String command) { + + public static final StartupConfig DEFAULT = new StartupConfig(false, ""); + +} diff --git a/server/src/main/java/org/phantazm/server/config/server/ZombiesGamereportConfig.java b/server/src/main/java/org/phantazm/server/config/server/ZombiesGamereportConfig.java new file mode 100644 index 000000000..c242805b0 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/config/server/ZombiesGamereportConfig.java @@ -0,0 +1,20 @@ +package org.phantazm.server.config.server; + +import org.jetbrains.annotations.NotNull; + +public record ZombiesGamereportConfig(@NotNull String idleStageFormat, + @NotNull String countdownStageFormat, + @NotNull String inGameFormat, + @NotNull String endedFormat, + @NotNull String gameEntryFormat, + @NotNull String nextPageFormat, + @NotNull String previousPageFormat, + @NotNull String pageFormat) { + public static final ZombiesGamereportConfig DEFAULT = + new ZombiesGamereportConfig("Idle", "Countdown", "Round - ", "Ended", + "/

: " + + "[WARP]", "Click here for the next page", + "Click here for the previous page", + "/ Zombies Gamereport at
Active " + + "Games
"); +} diff --git a/server/src/main/java/org/phantazm/server/config/zombies/ZombiesConfig.java b/server/src/main/java/org/phantazm/server/config/zombies/ZombiesConfig.java new file mode 100644 index 000000000..19f17e9c5 --- /dev/null +++ b/server/src/main/java/org/phantazm/server/config/zombies/ZombiesConfig.java @@ -0,0 +1,10 @@ +package org.phantazm.server.config.zombies; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.server.config.server.ZombiesGamereportConfig; + +public record ZombiesConfig(@NotNull ZombiesGamereportConfig gamereportConfig, int maximumScenes) { + + public static final ZombiesConfig DEFAULT = new ZombiesConfig(ZombiesGamereportConfig.DEFAULT, 20); + +} diff --git a/server/src/main/java/org/phantazm/server/permission/FilePermissionHandler.java b/server/src/main/java/org/phantazm/server/permission/FilePermissionHandler.java index 5252b3c73..54239fe61 100644 --- a/server/src/main/java/org/phantazm/server/permission/FilePermissionHandler.java +++ b/server/src/main/java/org/phantazm/server/permission/FilePermissionHandler.java @@ -60,6 +60,8 @@ public void applyPermissions(@NotNull UUID uuid, @NotNull CommandSender sender) applyTo(sender, group); } } + + applyTo(sender, EVERYONE_GROUP); } @Override @@ -113,6 +115,10 @@ public void addToGroup(@NotNull UUID uuid, @NotNull String group) { Objects.requireNonNull(uuid, "uuid"); Objects.requireNonNull(group, "group"); + if (group.equals(EVERYONE_GROUP)) { + return; + } + permissionData.groups().computeIfAbsent(uuid, ignored -> new CopyOnWriteArraySet<>()).add(group); Player player = MinecraftServer.getConnectionManager().getPlayer(uuid); @@ -126,6 +132,10 @@ public void removeFromGroup(@NotNull UUID uuid, @NotNull String group) { Objects.requireNonNull(uuid, "uuid"); Objects.requireNonNull(group, "group"); + if (group.equals(EVERYONE_GROUP)) { + return; + } + Map> playerGroups = permissionData.groups(); Set groups = playerGroups.get(uuid); if (groups != null) { @@ -158,6 +168,7 @@ private void synchronize(Player player) { } } + applyTo(player, EVERYONE_GROUP); player.sendPacket(MinecraftServer.getCommandManager().createDeclareCommandsPacket(player)); } diff --git a/server/src/main/java/org/phantazm/server/permission/PermissionHandler.java b/server/src/main/java/org/phantazm/server/permission/PermissionHandler.java index 42e325840..ca118beb1 100644 --- a/server/src/main/java/org/phantazm/server/permission/PermissionHandler.java +++ b/server/src/main/java/org/phantazm/server/permission/PermissionHandler.java @@ -7,6 +7,8 @@ import java.util.UUID; public interface PermissionHandler { + String EVERYONE_GROUP = "everyone"; + void applyPermissions(@NotNull UUID uuid, @NotNull CommandSender sender); void flush(); diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d7706d49..245f3626a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,7 +3,6 @@ rootProject.name = "phantazm" val localSettings = file("local.settings.gradle.kts") if (localSettings.exists()) { //apply from local settings too, if it exists - //can be used to sideload Minestom for faster testing apply(localSettings) } @@ -15,7 +14,6 @@ pluginManagement { name = "Fabric" } - maven("https://dl.cloudsmith.io/public/steanky/element/maven/") mavenCentral() gradlePluginPortal() @@ -34,10 +32,12 @@ sequenceOf( "proxima-minestom", "server", "stats", + "zombies-timer", "velocity", "zombies", "zombies-mapdata", - "zombies-mapeditor" + "zombies-mapeditor", + "snbt-builder" ).forEach { if (!toSkip.contains(it)) { include(":phantazm-$it") diff --git a/snbt-builder/build.gradle.kts b/snbt-builder/build.gradle.kts new file mode 100644 index 000000000..79763674f --- /dev/null +++ b/snbt-builder/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("phantazm.minestom-library-conventions") +} + +dependencies { + implementation(libs.adventure.text.minimessage) + implementation(libs.adventure.api) + implementation(libs.adventure.text.serializer.gson) + implementation(libs.hephaistos.common) + implementation(libs.hephaistos.gson) +} diff --git a/snbt-builder/src/main/java/org/phantazm/snbt/Builder.java b/snbt-builder/src/main/java/org/phantazm/snbt/Builder.java new file mode 100644 index 000000000..655d2171a --- /dev/null +++ b/snbt-builder/src/main/java/org/phantazm/snbt/Builder.java @@ -0,0 +1,31 @@ +package org.phantazm.snbt; + +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Builder { + public static void main(String[] args) { + if (args.length <= 1) { + System.out.println("Incorrect number of arguments; needs at least 2"); + System.exit(1); + return; + } + + String material = args[0]; + String name = args[1]; + List lore = Arrays.stream(args, 2, args.length).toList(); + + ItemStack itemStack = + ItemStack.builder(Objects.requireNonNullElse(Material.fromNamespaceId(material), Material.AIR)) + .displayName(MiniMessage.miniMessage().deserialize(name)) + .lore(lore.stream().map(string -> MiniMessage.miniMessage().deserialize(string)).toList()) + .build(); + + System.out.println(itemStack.toItemNBT().toSNBT()); + } +} diff --git a/stats/src/main/java/org/phantazm/stats/general/GeneralDatabase.java b/stats/src/main/java/org/phantazm/stats/general/GeneralDatabase.java new file mode 100644 index 000000000..33372eba5 --- /dev/null +++ b/stats/src/main/java/org/phantazm/stats/general/GeneralDatabase.java @@ -0,0 +1,13 @@ +package org.phantazm.stats.general; + +import org.jetbrains.annotations.NotNull; + +import java.time.ZonedDateTime; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface GeneralDatabase { + + @NotNull CompletableFuture handleJoin(@NotNull UUID playerUUID, @NotNull ZonedDateTime time); + +} diff --git a/stats/src/main/java/org/phantazm/stats/general/JooqGeneralSQLFetcher.java b/stats/src/main/java/org/phantazm/stats/general/JooqGeneralSQLFetcher.java new file mode 100644 index 000000000..9e3f71893 --- /dev/null +++ b/stats/src/main/java/org/phantazm/stats/general/JooqGeneralSQLFetcher.java @@ -0,0 +1,26 @@ +package org.phantazm.stats.general; + +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.impl.SQLDataType; + +import java.sql.Connection; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.jooq.impl.DSL.*; + +public class JooqGeneralSQLFetcher { + + public void handleJoin(@NotNull Connection connection, @NotNull UUID playerUUID, @NotNull ZonedDateTime time) { + long timestamp = time.toEpochSecond(); + DSLContext context = using(connection); + context.insertInto(table("phantazm_player_stats"), field("player_uuid"), field("first_join")) + .values(playerUUID.toString(), timestamp).onDuplicateKeyUpdate().set(field("first_join"), + when(field("first_join").isNull(), timestamp).otherwise(field("first_join", SQLDataType.BIGINT))) + .execute(); + + context.update(table("phantazm_player_stats")).set(field("last_join"), timestamp).execute(); + } + +} diff --git a/stats/src/main/java/org/phantazm/stats/general/SQLGeneralDatabase.java b/stats/src/main/java/org/phantazm/stats/general/SQLGeneralDatabase.java new file mode 100644 index 000000000..111015493 --- /dev/null +++ b/stats/src/main/java/org/phantazm/stats/general/SQLGeneralDatabase.java @@ -0,0 +1,51 @@ +package org.phantazm.stats.general; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.stats.zombies.SQLZombiesDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class SQLGeneralDatabase implements GeneralDatabase { + + private static final Logger LOGGER = LoggerFactory.getLogger(SQLZombiesDatabase.class); + + private final Executor executor; + + private final DataSource dataSource; + + private final JooqGeneralSQLFetcher sqlFetcher; + + public SQLGeneralDatabase(@NotNull Executor executor, @NotNull DataSource dataSource, + @NotNull JooqGeneralSQLFetcher sqlFetcher) { + this.executor = Objects.requireNonNull(executor, "executor"); + this.dataSource = Objects.requireNonNull(dataSource, "dataSource"); + this.sqlFetcher = Objects.requireNonNull(sqlFetcher, "sqlFetcher"); + } + + @Override + public @NotNull CompletableFuture handleJoin(@NotNull UUID playerUUID, @NotNull ZonedDateTime time) { + return CompletableFuture.runAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + sqlFetcher.handleJoin(connection, playerUUID, time); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + }, executor).whenComplete(this::logException); + } + + private void logException(T ignored, Throwable throwable) { + if (throwable != null) { + LOGGER.warn("Exception while querying database", throwable); + } + } +} diff --git a/stats/src/main/java/org/phantazm/stats/zombies/BasicZombiesPlayerMapStats.java b/stats/src/main/java/org/phantazm/stats/zombies/BasicZombiesPlayerMapStats.java index c3e433059..5724f4b71 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/BasicZombiesPlayerMapStats.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/BasicZombiesPlayerMapStats.java @@ -20,13 +20,15 @@ public class BasicZombiesPlayerMapStats implements ZombiesPlayerMapStats { private Long bestTime; + private int bestRound; + private int roundsSurvived; private int kills; - private int coinsGained; + private long coinsGained; - private int coinsSpent; + private long coinsSpent; private int knocks; @@ -41,13 +43,14 @@ public class BasicZombiesPlayerMapStats implements ZombiesPlayerMapStats { private int headshotHits; public BasicZombiesPlayerMapStats(@NotNull UUID playerUUID, @NotNull Key mapKey, int gamesPlayed, int wins, - @Nullable Long bestTime, int roundsSurvived, int kills, int knocks, int coinsGained, int coinsSpent, - int deaths, int revives, int shots, int regularHits, int headshotHits) { + @Nullable Long bestTime, int bestRound, int roundsSurvived, int kills, int knocks, long coinsGained, + long coinsSpent, int deaths, int revives, int shots, int regularHits, int headshotHits) { this.playerUUID = Objects.requireNonNull(playerUUID, "playerUUID"); this.mapKey = Objects.requireNonNull(mapKey, "mapKey"); this.gamesPlayed = gamesPlayed; this.wins = wins; this.bestTime = bestTime; + this.bestRound = bestRound; this.roundsSurvived = roundsSurvived; this.kills = kills; this.coinsGained = coinsGained; @@ -61,7 +64,7 @@ public BasicZombiesPlayerMapStats(@NotNull UUID playerUUID, @NotNull Key mapKey, } public static ZombiesPlayerMapStats createBasicStats(@NotNull UUID playerUUID, @NotNull Key mapKey) { - return new BasicZombiesPlayerMapStats(playerUUID, mapKey, 0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + return new BasicZombiesPlayerMapStats(playerUUID, mapKey, 0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); } @Override @@ -104,6 +107,16 @@ public void setBestTime(@Nullable Long bestTime) { this.bestTime = bestTime; } + @Override + public int getBestRound() { + return bestRound; + } + + @Override + public void setBestRound(int bestRound) { + this.bestRound = bestRound; + } + @Override public int getRoundsSurvived() { return roundsSurvived; @@ -125,22 +138,22 @@ public void setKills(int kills) { } @Override - public int getCoinsGained() { + public long getCoinsGained() { return coinsGained; } @Override - public void setCoinsGained(int coinsGained) { + public void setCoinsGained(long coinsGained) { this.coinsGained = coinsGained; } @Override - public int getCoinsSpent() { + public long getCoinsSpent() { return coinsSpent; } @Override - public void setCoinsSpent(int coinsSpent) { + public void setCoinsSpent(long coinsSpent) { this.coinsSpent = coinsSpent; } diff --git a/stats/src/main/java/org/phantazm/stats/zombies/BestTime.java b/stats/src/main/java/org/phantazm/stats/zombies/BestTime.java index f1adc0d32..759e8eed5 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/BestTime.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/BestTime.java @@ -4,5 +4,5 @@ import java.util.UUID; -public record BestTime(@NotNull UUID uuid, long time) { +public record BestTime(int rank, @NotNull UUID uuid, long time) { } diff --git a/stats/src/main/java/org/phantazm/stats/zombies/JooqZombiesSQLFetcher.java b/stats/src/main/java/org/phantazm/stats/zombies/JooqZombiesSQLFetcher.java index 30b3cb70b..06e4acb1c 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/JooqZombiesSQLFetcher.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/JooqZombiesSQLFetcher.java @@ -2,6 +2,8 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; +import org.jooq.Record; +import org.jooq.Record2; import org.jooq.impl.SQLDataType; import java.sql.Connection; @@ -9,6 +11,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.jooq.impl.DSL.*; @@ -19,24 +22,27 @@ public class JooqZombiesSQLFetcher implements ZombiesSQLFetcher { public void synchronizeZombiesPlayerMapStats(@NotNull Connection connection, @NotNull ZombiesPlayerMapStats mapStats) { using(connection).insertInto(table("zombies_player_map_stats"), field("player_uuid"), field("map_key"), - field("games_played"), field("wins"), field("best_time"), field("rounds_survived"), field("kills"), - field("coins_gained"), field("coins_spent"), field("knocks"), field("deaths"), field("revives"), - field("shots"), field("regular_hits"), field("headshot_hits")) + field("games_played"), field("wins"), field("best_time"), field("best_round"), field("rounds_survived"), + field("kills"), field("coins_gained"), field("coins_spent"), field("knocks"), field("deaths"), + field("revives"), field("shots"), field("regular_hits"), field("headshot_hits")) .values(mapStats.getPlayerUUID().toString(), mapStats.getMapKey().asString(), mapStats.getGamesPlayed(), - mapStats.getWins(), mapStats.getBestTime().orElse(null), mapStats.getRoundsSurvived(), - mapStats.getKills(), mapStats.getCoinsGained(), mapStats.getCoinsSpent(), mapStats.getKnocks(), - mapStats.getDeaths(), mapStats.getRevives(), mapStats.getShots(), mapStats.getRegularHits(), - mapStats.getHeadshotHits()).onDuplicateKeyUpdate() + mapStats.getWins(), mapStats.getBestTime().orElse(null), mapStats.getBestRound(), + mapStats.getRoundsSurvived(), mapStats.getKills(), mapStats.getCoinsGained(), + mapStats.getCoinsSpent(), mapStats.getKnocks(), mapStats.getDeaths(), mapStats.getRevives(), + mapStats.getShots(), mapStats.getRegularHits(), mapStats.getHeadshotHits()) + .onDuplicateKeyUpdate() .set(field("games_played"), field("games_played", SQLDataType.INTEGER).plus(mapStats.getGamesPlayed())) .set(field("wins"), field("wins", SQLDataType.INTEGER).plus(mapStats.getWins())).set(field("best_time"), - mapStats.getBestTime().isPresent() ? when(field("best_time").isNotNull(), - least(field("best_time"), mapStats.getBestTime().get())).otherwise( - inline(null, SQLDataType.INTEGER)) : field("best_time", SQLDataType.BIGINT)) + mapStats.getBestTime().isPresent() + ? when(field("best_time").isNotNull(), + least(field("best_time"), mapStats.getBestTime().get())).otherwise(mapStats.getBestTime().get()) + : field("best_time", SQLDataType.BIGINT)).set(field("best_round"), + greatest(field("best_round", SQLDataType.INTEGER), val(mapStats.getBestRound()))) .set(field("rounds_survived"), field("rounds_survived", SQLDataType.INTEGER).plus(mapStats.getRoundsSurvived())) .set(field("kills"), field("kills", SQLDataType.INTEGER).plus(mapStats.getRoundsSurvived())) - .set(field("coins_gained"), field("coins_gained", SQLDataType.INTEGER).plus(mapStats.getCoinsGained())) - .set(field("coins_spent"), field("coins_spent", SQLDataType.INTEGER).plus(mapStats.getCoinsSpent())) + .set(field("coins_gained"), field("coins_gained", SQLDataType.BIGINT).plus(mapStats.getCoinsGained())) + .set(field("coins_spent"), field("coins_spent", SQLDataType.BIGINT).plus(mapStats.getCoinsSpent())) .set(field("knocks"), field("knocks", SQLDataType.INTEGER).plus(mapStats.getKnocks())) .set(field("deaths"), field("deaths", SQLDataType.INTEGER).plus(mapStats.getDeaths())) .set(field("revives"), field("revives", SQLDataType.INTEGER).plus(mapStats.getRevives())) @@ -47,19 +53,58 @@ public void synchronizeZombiesPlayerMapStats(@NotNull Connection connection, } @Override - public @NotNull List getBestTimes(@NotNull Connection connection, @NotNull Key mapKey) + public @NotNull ZombiesPlayerMapStats getMapStats(@NotNull Connection connection, @NotNull UUID playerUUID, + @NotNull Key mapKey) { + Record result = using(connection).select().from(table("zombies_player_map_stats")) + .where(field("player_uuid").eq(playerUUID.toString())).and(field("map_key").eq(mapKey.asString())) + .fetchOne(); + if (result == null) { + return BasicZombiesPlayerMapStats.createBasicStats(playerUUID, mapKey); + } + + return new BasicZombiesPlayerMapStats(playerUUID, mapKey, result.get("games_played", int.class), + result.get("wins", int.class), result.get("best_time", Long.class), result.get("best_round", int.class), + result.get("rounds_survived", int.class), result.get("kills", int.class), + result.get("knocks", int.class), result.get("coins_gained", int.class), + result.get("coins_spent", int.class), result.get("deaths", int.class), result.get("revives", int.class), + result.get("shots", int.class), result.get("regular_hits", int.class), + result.get("headshot_hits", int.class)); + } + + @Override + public @NotNull List getBestTimes(@NotNull Connection connection, @NotNull Key mapKey, int maxLength) throws SQLException { List bestTimes = new ArrayList<>(); try (ResultSet resultSet = using(connection).select(field("player_uuid"), field("best_time")) .from(table("zombies_player_map_stats")).where(field("map_key").eq(mapKey.asString())) - .and(field("best_time").isNotNull()).orderBy(field("best_time")).fetchResultSet()) { + .and(field("best_time").isNotNull()).orderBy(field("best_time"), field("player_uuid")).limit(maxLength) + .fetchResultSet()) { + int i = 0; while (resultSet.next()) { UUID uuid = UUID.fromString(resultSet.getString("player_uuid")); long bestTime = resultSet.getLong("best_time"); - bestTimes.add(new BestTime(uuid, bestTime)); + bestTimes.add(new BestTime(++i, uuid, bestTime)); } } return bestTimes; } + + @Override + public @NotNull Optional getBestTime(@NotNull Connection connection, @NotNull UUID playerUUID, + @NotNull Key mapKey) { + Record2 result = + using(connection).select(field("best_time", SQLDataType.BIGINT), field("rank", SQLDataType.INTEGER)) + .from(select(field("best_time"), field("player_uuid"), + rowNumber().over(orderBy(field("best_time"), field("player_uuid"))).as("rank")).from( + table("zombies_player_map_stats")).where(field("map_key").eq(mapKey.asString())) + .and(field("best_time").isNotNull())) + .where(field("player_uuid").eq(playerUUID.toString())).fetchOne(); + if (result == null) { + return Optional.empty(); + } + + return Optional.of(new BestTime(result.value2(), playerUUID, result.value1())); + } + } diff --git a/stats/src/main/java/org/phantazm/stats/zombies/SQLZombiesDatabase.java b/stats/src/main/java/org/phantazm/stats/zombies/SQLZombiesDatabase.java index d00444d42..3fb1ed239 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/SQLZombiesDatabase.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/SQLZombiesDatabase.java @@ -10,6 +10,8 @@ import java.sql.SQLException; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -43,10 +45,35 @@ public SQLZombiesDatabase(@NotNull Executor executor, @NotNull DataSource dataSo } @Override - public CompletableFuture> getBestTimes(@NotNull Key mapKey) { + public @NotNull CompletableFuture getMapStats(@NotNull UUID playerUUID, + @NotNull Key mapKey) { return CompletableFuture.supplyAsync(() -> { try (Connection connection = dataSource.getConnection()) { - return sqlFetcher.getBestTimes(connection, mapKey); + return sqlFetcher.getMapStats(connection, playerUUID, mapKey); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + }, executor).whenComplete(this::logException); + } + + @Override + public @NotNull CompletableFuture> getBestTimes(@NotNull Key mapKey, int maxLength) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + return sqlFetcher.getBestTimes(connection, mapKey, maxLength); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + }, executor).whenComplete(this::logException); + } + + @Override + public @NotNull CompletableFuture> getBestTime(@NotNull UUID playerUUID, @NotNull Key mapKey) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + return sqlFetcher.getBestTime(connection, playerUUID, mapKey); } catch (SQLException e) { throw new RuntimeException(e); diff --git a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesDatabase.java b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesDatabase.java index a07821b51..55878dad8 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesDatabase.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesDatabase.java @@ -4,11 +4,20 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; public interface ZombiesDatabase { @NotNull CompletableFuture synchronizeZombiesPlayerMapStats(@NotNull ZombiesPlayerMapStats stats); - CompletableFuture> getBestTimes(@NotNull Key mapKey); + @NotNull CompletableFuture getMapStats(@NotNull UUID playerUUID, @NotNull Key mapKey); + + @NotNull CompletableFuture> getBestTimes(@NotNull Key mapKey, int maxLength); + + @NotNull CompletableFuture> getBestTime(@NotNull UUID playerUUID, @NotNull Key mapKey); + + + } diff --git a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesPlayerMapStats.java b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesPlayerMapStats.java index 21060b812..a393bb2df 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesPlayerMapStats.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesPlayerMapStats.java @@ -25,6 +25,10 @@ public interface ZombiesPlayerMapStats { void setBestTime(@Nullable Long bestTime); + int getBestRound(); + + void setBestRound(int bestRound); + int getRoundsSurvived(); void setRoundsSurvived(int roundsSurvived); @@ -33,13 +37,13 @@ public interface ZombiesPlayerMapStats { void setKills(int kills); - int getCoinsGained(); + long getCoinsGained(); - void setCoinsGained(int goldGained); + void setCoinsGained(long goldGained); - int getCoinsSpent(); + long getCoinsSpent(); - void setCoinsSpent(int goldSpent); + void setCoinsSpent(long goldSpent); int getKnocks(); diff --git a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesSQLFetcher.java b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesSQLFetcher.java index 4dea92b8e..f5c515854 100644 --- a/stats/src/main/java/org/phantazm/stats/zombies/ZombiesSQLFetcher.java +++ b/stats/src/main/java/org/phantazm/stats/zombies/ZombiesSQLFetcher.java @@ -6,11 +6,18 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.List; +import java.util.Optional; +import java.util.UUID; public interface ZombiesSQLFetcher { void synchronizeZombiesPlayerMapStats(@NotNull Connection connection, @NotNull ZombiesPlayerMapStats mapStats); - @NotNull List getBestTimes(@NotNull Connection connection, @NotNull Key mapKey) throws SQLException; + @NotNull ZombiesPlayerMapStats getMapStats(@NotNull Connection connection, @NotNull UUID playerUUID, + @NotNull Key mapKey); + @NotNull List getBestTimes(@NotNull Connection connection, @NotNull Key mapKey, int maxLength) throws SQLException; + + @NotNull Optional getBestTime(@NotNull Connection connection, @NotNull UUID playerUUID, + @NotNull Key mapKey); } diff --git a/velocity/src/main/java/org/phantazm/velocity/PhantazmPlugin.java b/velocity/src/main/java/org/phantazm/velocity/PhantazmPlugin.java index 48ecc4fa2..130b8b5b1 100644 --- a/velocity/src/main/java/org/phantazm/velocity/PhantazmPlugin.java +++ b/velocity/src/main/java/org/phantazm/velocity/PhantazmPlugin.java @@ -1,18 +1,19 @@ package org.phantazm.velocity; import com.google.inject.Inject; +import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import org.phantazm.commons.Namespaces; -import org.phantazm.messaging.MessageChannels; +import org.phantazm.messaging.packet.player.MapDataVersionQueryPacket; import org.phantazm.velocity.listener.MaliciousPluginMessageBlocker; import org.phantazm.velocity.listener.ProtocolVersionForwarder; import org.phantazm.velocity.listener.ProxyMessagingHandler; +import java.util.Collections; + /** * A velocity plugin used to communicate information to the Phantazm server. */ @@ -39,15 +40,12 @@ public PhantazmPlugin(ProxyServer server) { */ @Subscribe public void onInitialize(ProxyInitializeEvent event) { - ChannelIdentifier proxyToServer = - MinecraftChannelIdentifier.create(Namespaces.PHANTAZM, MessageChannels.PROXY_TO_SERVER); - ChannelIdentifier clientToProxy = MinecraftChannelIdentifier.create(Namespaces.PHANTAZM, - MessageChannels.CLIENT_TO_PROXY); - server.getChannelRegistrar().register(proxyToServer); - server.getChannelRegistrar().register(clientToProxy); - server.getEventManager().register(this, new MaliciousPluginMessageBlocker(proxyToServer)); - server.getEventManager().register(this, new ProtocolVersionForwarder()); - server.getEventManager().register(this, new ProxyMessagingHandler(clientToProxy)); + server.getChannelRegistrar().register(MinecraftChannelIdentifier.from(MapDataVersionQueryPacket.ID)); + + EventManager eventManager = server.getEventManager(); + eventManager.register(this, new MaliciousPluginMessageBlocker(Collections.emptySet())); + eventManager.register(this, new ProtocolVersionForwarder()); + eventManager.register(this, ProxyMessagingHandler.createDefault()); } } diff --git a/velocity/src/main/java/org/phantazm/velocity/listener/MaliciousPluginMessageBlocker.java b/velocity/src/main/java/org/phantazm/velocity/listener/MaliciousPluginMessageBlocker.java index 370d81704..d78e292aa 100644 --- a/velocity/src/main/java/org/phantazm/velocity/listener/MaliciousPluginMessageBlocker.java +++ b/velocity/src/main/java/org/phantazm/velocity/listener/MaliciousPluginMessageBlocker.java @@ -9,21 +9,17 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; +import java.util.Set; /** * Blocks malicious plugin messages from players that go through the plugin's proxy channel. */ public class MaliciousPluginMessageBlocker { - private final ChannelIdentifier proxyIdentifier; + private final Set blockedIdentifiers; - /** - * Creates a {@link MaliciousPluginMessageBlocker}. - * - * @param channelIdentifier The {@link ChannelIdentifier} for the plugin's proxy2server channel - */ - public MaliciousPluginMessageBlocker(@NotNull ChannelIdentifier channelIdentifier) { - this.proxyIdentifier = Objects.requireNonNull(channelIdentifier, "channelIdentifier"); + public MaliciousPluginMessageBlocker(@NotNull Set blockedIdentifiers) { + this.blockedIdentifiers = Objects.requireNonNull(blockedIdentifiers, "blockedIdentifiers"); } /** @@ -33,8 +29,7 @@ public MaliciousPluginMessageBlocker(@NotNull ChannelIdentifier channelIdentifie */ @Subscribe public void onPlayerMessage(PluginMessageEvent event) { - if (event.getIdentifier().getId().equals(proxyIdentifier.getId()) && - event.getSource() instanceof Player player) { + if (blockedIdentifiers.contains(event.getIdentifier()) && event.getSource() instanceof Player player) { event.setResult(PluginMessageEvent.ForwardResult.handled()); player.disconnect(Component.text("Malicious plugin message.", NamedTextColor.RED)); } diff --git a/velocity/src/main/java/org/phantazm/velocity/listener/ProxyMessagingHandler.java b/velocity/src/main/java/org/phantazm/velocity/listener/ProxyMessagingHandler.java index 12c3910ef..1110e10e3 100644 --- a/velocity/src/main/java/org/phantazm/velocity/listener/ProxyMessagingHandler.java +++ b/velocity/src/main/java/org/phantazm/velocity/listener/ProxyMessagingHandler.java @@ -1,56 +1,45 @@ package org.phantazm.velocity.listener; -import com.google.common.io.ByteStreams; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import org.jetbrains.annotations.NotNull; -import org.phantazm.messaging.packet.Packet; -import org.phantazm.messaging.packet.PacketHandler; -import org.phantazm.messaging.packet.c2p.MapDataVersionQueryPacket; -import org.phantazm.messaging.packet.c2p.MapDataVersionResponsePacket; -import org.phantazm.messaging.serialization.PacketSerializer; -import org.phantazm.messaging.serialization.PacketSerializers; -import org.phantazm.velocity.packet.ByteArrayInputDataReader; -import org.phantazm.velocity.packet.ByteArrayOutputDataWriter; +import org.phantazm.messaging.packet.player.MapDataVersionQueryPacket; +import org.phantazm.messaging.packet.proxy.MapDataVersionResponsePacket; +import org.phantazm.velocity.packet.VelocityPacketUtils; import org.phantazm.zombies.map.MapSettingsInfo; import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; /** * Handles all messaging that involves the proxy */ public class ProxyMessagingHandler { - private final Map> packetHandlers; + private final Map> packetHandlers; /** * Creates a new {@link ProxyMessagingHandler}. - * @param clientToProxyIdentifier The identifier for the channel between the client and the proxy */ - @SuppressWarnings("UnstableApiUsage") - public ProxyMessagingHandler(@NotNull ChannelIdentifier clientToProxyIdentifier) { - PacketSerializer clientToProxy = PacketSerializers.clientToProxySerializer( - () -> new ByteArrayOutputDataWriter(ByteStreams.newDataOutput()), - data -> new ByteArrayInputDataReader(ByteStreams.newDataInput(data))); - packetHandlers = Map.of(clientToProxyIdentifier, new PacketHandler<>(clientToProxy) { - @Override - protected void handlePacket(@NotNull Player player, @NotNull Packet packet) { - if (packet instanceof MapDataVersionQueryPacket) { - output(player, new MapDataVersionResponsePacket(MapSettingsInfo.MAP_DATA_VERSION)); - } - } + public ProxyMessagingHandler(@NotNull Map> packetHandlers) { + this.packetHandlers = Objects.requireNonNull(packetHandlers, "packetHandlers"); + } - @Override - protected void sendToReceiver(@NotNull Player player, byte @NotNull [] data) { - player.sendPluginMessage(clientToProxyIdentifier, data); - } + public static @NotNull ProxyMessagingHandler createDefault() { + Map> packetHandlers = Map.of(MinecraftChannelIdentifier.from(MapDataVersionQueryPacket.ID), (player, data) -> { + VelocityPacketUtils.sendPacket(player, new MapDataVersionResponsePacket(MapSettingsInfo.MAP_DATA_VERSION)); }); + + return new ProxyMessagingHandler(packetHandlers); } /** * Handles a {@link PluginMessageEvent}. + * * @param event The event */ @Subscribe @@ -59,9 +48,9 @@ public void onPluginMessage(PluginMessageEvent event) { return; } - PacketHandler packetHandler = packetHandlers.get(event.getIdentifier()); + BiConsumer packetHandler = packetHandlers.get(event.getIdentifier()); if (packetHandler != null) { - packetHandler.handleData(player, event.getData()); + packetHandler.accept(player, event.getData()); event.setResult(PluginMessageEvent.ForwardResult.handled()); } } diff --git a/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayInputDataReader.java b/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayInputDataReader.java index a292070f2..4da5d1323 100644 --- a/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayInputDataReader.java +++ b/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayInputDataReader.java @@ -1,6 +1,7 @@ package org.phantazm.velocity.packet; import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; import org.jetbrains.annotations.NotNull; import org.phantazm.messaging.serialization.DataReader; @@ -22,6 +23,10 @@ public ByteArrayInputDataReader(@NotNull ByteArrayDataInput input) { this.input = Objects.requireNonNull(input, "input"); } + public ByteArrayInputDataReader(byte @NotNull[] bytes) { + this(ByteStreams.newDataInput(bytes)); + } + @Override public byte readByte() { return input.readByte(); diff --git a/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayOutputDataWriter.java b/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayOutputDataWriter.java index f70856d84..57126adb4 100644 --- a/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayOutputDataWriter.java +++ b/velocity/src/main/java/org/phantazm/velocity/packet/ByteArrayOutputDataWriter.java @@ -1,6 +1,7 @@ package org.phantazm.velocity.packet; import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import org.jetbrains.annotations.NotNull; import org.phantazm.messaging.serialization.DataWriter; @@ -22,6 +23,10 @@ public ByteArrayOutputDataWriter(@NotNull ByteArrayDataOutput output) { this.output = Objects.requireNonNull(output, "output"); } + public ByteArrayOutputDataWriter() { + this(ByteStreams.newDataOutput()); + } + @Override public void writeByte(byte data) { output.writeByte(data); diff --git a/velocity/src/main/java/org/phantazm/velocity/packet/VelocityPacketUtils.java b/velocity/src/main/java/org/phantazm/velocity/packet/VelocityPacketUtils.java new file mode 100644 index 000000000..62f864c15 --- /dev/null +++ b/velocity/src/main/java/org/phantazm/velocity/packet/VelocityPacketUtils.java @@ -0,0 +1,24 @@ +package org.phantazm.velocity.packet; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.Packet; +import org.phantazm.messaging.packet.proxy.MapDataVersionResponsePacket; +import org.phantazm.messaging.serialization.DataWriter; +import org.phantazm.zombies.map.MapSettingsInfo; + +public class VelocityPacketUtils { + + public static byte @NotNull[] serialize(@NotNull Packet packet) { + DataWriter dataWriter = new ByteArrayOutputDataWriter(); + packet.write(dataWriter); + return dataWriter.toByteArray(); + } + + public static void sendPacket(@NotNull Player player, @NotNull Packet packet) { + byte[] data = VelocityPacketUtils.serialize(new MapDataVersionResponsePacket(MapSettingsInfo.MAP_DATA_VERSION)); + player.sendPluginMessage(MinecraftChannelIdentifier.from(packet.getId()), data); + } + +} diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/Evaluation.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/Evaluation.java index 5277486a8..cab20b7fa 100644 --- a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/Evaluation.java +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/Evaluation.java @@ -3,19 +3,19 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; -import java.util.function.Predicate; +import java.util.function.BiPredicate; public enum Evaluation { ALL_TRUE, ANY_TRUE; - public boolean evaluate(@NotNull Iterable> predicates, T type) { + public boolean evaluate(@NotNull Iterable> predicates, T type, V second) { Objects.requireNonNull(predicates, "predicates"); return switch (this) { case ALL_TRUE -> { - for (Predicate predicate : predicates) { - if (!predicate.test(type)) { + for (BiPredicate predicate : predicates) { + if (!predicate.test(type, second)) { yield false; } } @@ -23,8 +23,8 @@ public boolean evaluate(@NotNull Iterable> predicates yield true; } case ANY_TRUE -> { - for (Predicate predicate : predicates) { - if (predicate.test(type)) { + for (BiPredicate predicate : predicates) { + if (predicate.test(type, second)) { yield true; } } diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/FileSystemMapLoader.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/FileSystemMapLoader.java index 0b411a79f..65723333f 100644 --- a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/FileSystemMapLoader.java +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/FileSystemMapLoader.java @@ -28,7 +28,6 @@ public class FileSystemMapLoader extends FilesystemLoader { private static final String ROUNDS_PATH = "rounds"; private static final String SPAWNRULES_PATH = "spawnrules"; private static final String SPAWNPOINTS_PATH = "spawnpoints"; - private static final String SIDEBAR_PATH = "sidebar"; private final String mapInfoName; private final BiPredicate configPredicate; @@ -74,7 +73,6 @@ public FileSystemMapLoader(@NotNull Path root, @NotNull ConfigCodec codec) { Path mapInfoFile = mapDirectory.resolve(mapInfoName); - MapSettingsInfo mapSettingsInfo = Configuration.read(mapInfoFile, codec, MapProcessors.mapInfo()); FolderPaths paths = new FolderPaths(mapDirectory); List rooms = new ArrayList<>(); List doors = new ArrayList<>(); @@ -107,17 +105,34 @@ public FileSystemMapLoader(@NotNull Path root, @NotNull ConfigCodec codec) { file -> spawnpoints.add(Configuration.read(file, codec, MapProcessors.spawnpointInfo()))); String sidebarSettingsPath = - "settings" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + "sidebar" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + ConfigNode scoreboard = + Configuration.read(mapDirectory.resolve(sidebarSettingsPath), codec, MapProcessors.sidebar()); + String corpsePath = "corpse" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + ConfigNode corpse = Configuration.read(mapDirectory.resolve(corpsePath), codec).asNode(); - ConfigNode scoreboard = - Configuration.read(paths.sidebar.resolve(sidebarSettingsPath), codec, MapProcessors.sidebar()); + String coinsPath = + "coins" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + PlayerCoinsInfo playerCoins = + Configuration.read(mapDirectory.resolve(coinsPath), codec, MapProcessors.playerCoinsInfo()); - ConfigNode corpse = Configuration.read(mapDirectory.resolve(corpsePath), codec).asNode(); + String leaderboardPath = + "leaderboard" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + LeaderboardInfo leaderboard = + Configuration.read(mapDirectory.resolve(leaderboardPath), codec, MapProcessors.leaderboardInfo()); + + String webhookPath = + "webhook" + (codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension()); + WebhookInfo webhook = + Configuration.read(mapDirectory.resolve(webhookPath), codec, MapProcessors.webhookInfo()); + + + MapSettingsInfo mapSettingsInfo = Configuration.read(mapInfoFile, codec, MapProcessors.mapInfo()); - return new MapInfo(mapSettingsInfo, rooms, doors, shops, windows, rounds, spawnrules, spawnpoints, scoreboard, - corpse); + return new MapInfo(mapSettingsInfo, playerCoins, rooms, doors, shops, windows, rounds, spawnrules, spawnpoints, + leaderboard, scoreboard, corpse, webhook); } @Override @@ -138,7 +153,6 @@ public void save(@NotNull MapInfo data) throws IOException { FileUtils.deleteRecursivelyIfExists(paths.rounds); FileUtils.deleteRecursivelyIfExists(paths.spawnrules); FileUtils.deleteRecursivelyIfExists(paths.spawnpoints); - FileUtils.deleteRecursivelyIfExists(paths.sidebar); FileUtils.createDirectories(paths.rooms); FileUtils.createDirectories(paths.doors); @@ -147,7 +161,6 @@ public void save(@NotNull MapInfo data) throws IOException { FileUtils.createDirectories(paths.rounds); FileUtils.createDirectories(paths.spawnrules); FileUtils.createDirectories(paths.spawnpoints); - FileUtils.createDirectories(paths.sidebar); String extension = codec.getPreferredExtensions().isEmpty() ? "" : "." + codec.getPreferredExtension(); for (RoomInfo room : data.rooms()) { @@ -194,7 +207,7 @@ public void save(@NotNull MapInfo data) throws IOException { MapProcessors.spawnpointInfo().elementFromData(spawnpoint), codec); } - Configuration.write(paths.sidebar.resolve("settings" + extension), data.scoreboard(), codec); + Configuration.write(mapDirectory.resolve("sidebar" + extension), data.scoreboard(), codec); Configuration.write(mapDirectory.resolve("corpse" + extension), data.corpse(), codec); } @@ -229,12 +242,11 @@ private record FolderPaths(Path rooms, Path windows, Path rounds, Path spawnrules, - Path spawnpoints, - Path sidebar) { + Path spawnpoints) { private FolderPaths(Path root) { this(root.resolve(ROOMS_PATH), root.resolve(DOORS_PATH), root.resolve(SHOPS_PATH), root.resolve(WINDOWS_PATH), root.resolve(ROUNDS_PATH), root.resolve(SPAWNRULES_PATH), - root.resolve(SPAWNPOINTS_PATH), root.resolve(SIDEBAR_PATH)); + root.resolve(SPAWNPOINTS_PATH)); } } } diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/LeaderboardInfo.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/LeaderboardInfo.java new file mode 100644 index 000000000..c9202ffca --- /dev/null +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/LeaderboardInfo.java @@ -0,0 +1,12 @@ +package org.phantazm.zombies.map; + +import com.github.steanky.ethylene.core.collection.ConfigNode; +import com.github.steanky.ethylene.core.collection.LinkedConfigNode; +import com.github.steanky.vector.Vec3D; +import org.jetbrains.annotations.NotNull; + +public record LeaderboardInfo(@NotNull Vec3D location, double gap, @NotNull ConfigNode data) { + + public static final LeaderboardInfo DEFAULT = new LeaderboardInfo(Vec3D.ORIGIN, 0.1, new LinkedConfigNode(0)); + +} diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapInfo.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapInfo.java index 3ae5bb5a1..becdf70f8 100644 --- a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapInfo.java +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapInfo.java @@ -12,6 +12,7 @@ * Represents a Zombies map. */ public record MapInfo(@NotNull MapSettingsInfo settings, + @NotNull PlayerCoinsInfo playerCoins, @NotNull List rooms, @NotNull List doors, @NotNull List shops, @@ -19,8 +20,10 @@ public record MapInfo(@NotNull MapSettingsInfo settings, @NotNull List rounds, @NotNull List spawnrules, @NotNull List spawnpoints, + @NotNull LeaderboardInfo leaderboard, @NotNull ConfigNode scoreboard, - @NotNull ConfigNode corpse) implements Keyed { + @NotNull ConfigNode corpse, + @NotNull WebhookInfo webhook) implements Keyed { /** * Constructs a new instances of this record. * @@ -34,11 +37,13 @@ public record MapInfo(@NotNull MapSettingsInfo settings, * @param spawnpoints this map's spawnpoints * @param scoreboard this map's scoreboard info */ - public MapInfo(@NotNull MapSettingsInfo settings, @NotNull List rooms, @NotNull List doors, - @NotNull List shops, @NotNull List windows, @NotNull List rounds, - @NotNull List spawnrules, @NotNull List spawnpoints, - @NotNull ConfigNode scoreboard, @NotNull ConfigNode corpse) { + public MapInfo(@NotNull MapSettingsInfo settings, @NotNull PlayerCoinsInfo playerCoins, + @NotNull List rooms, @NotNull List doors, @NotNull List shops, + @NotNull List windows, @NotNull List rounds, @NotNull List spawnrules, + @NotNull List spawnpoints, @NotNull LeaderboardInfo leaderboard, + @NotNull ConfigNode scoreboard, @NotNull ConfigNode corpse, @NotNull WebhookInfo webhook) { this.settings = Objects.requireNonNull(settings, "settings"); + this.playerCoins = Objects.requireNonNull(playerCoins, "playerCoins"); this.rooms = Objects.requireNonNull(rooms, "rooms"); this.doors = Objects.requireNonNull(doors, "doors"); this.shops = Objects.requireNonNull(shops, "shops"); @@ -46,8 +51,10 @@ public MapInfo(@NotNull MapSettingsInfo settings, @NotNull List rooms, this.rounds = Objects.requireNonNull(rounds, "rounds"); this.spawnrules = Objects.requireNonNull(spawnrules, "spawnrules"); this.spawnpoints = Objects.requireNonNull(spawnpoints, "spawnpoints"); + this.leaderboard = Objects.requireNonNull(leaderboard, "leaderboard"); this.scoreboard = Objects.requireNonNull(scoreboard, "scoreboard"); this.corpse = Objects.requireNonNull(corpse, "corpse"); + this.webhook = Objects.requireNonNull(webhook, "webhook"); } @Override diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapProcessors.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapProcessors.java index 487c08985..7cf6dd6e8 100644 --- a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapProcessors.java +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapProcessors.java @@ -15,8 +15,11 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.ConfigProcessors; +import org.phantazm.commons.chat.ChatDestination; +import org.phantazm.commons.chat.MessageWithDestination; import org.phantazm.commons.vector.VectorConfigProcessors; import java.util.*; @@ -81,6 +84,41 @@ public SpawnpointInfo dataFromElement(@NotNull ConfigElement element) throws Con } }; + private static final ConfigProcessor leaderboardInfo = new ConfigProcessor<>() { + @Override + public LeaderboardInfo dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { + Vec3D location = VectorConfigProcessors.vec3D().dataFromElement(element.getElementOrThrow("location")); + double gap = element.getNumberOrThrow("gap").doubleValue(); + ConfigNode data = element.getNodeOrThrow("data"); + + return new LeaderboardInfo(location, gap, data); + } + + @Override + public @NotNull ConfigElement elementFromData(LeaderboardInfo leaderboardInfo) throws ConfigProcessException { + return ConfigNode.of("location", VectorConfigProcessors.vec3D().elementFromData(leaderboardInfo.location()), + "gap", leaderboardInfo.gap(), "data", leaderboardInfo.data()); + } + }; + + private static final ConfigProcessor webhookInfo = new ConfigProcessor<>() { + @Override + public WebhookInfo dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { + String webhookURL = element.getStringOrThrow("webhookURL"); + String webhookFormat = element.getStringOrThrow("webhookFormat"); + String playerFormat = element.getStringOrThrow("playerFormat"); + boolean enabled = element.getBooleanOrDefault(false, "enabled"); + + return new WebhookInfo(webhookURL, webhookFormat, playerFormat, enabled); + } + + @Override + public @NotNull ConfigElement elementFromData(WebhookInfo webhookInfo) { + return ConfigNode.of("webhookURL", webhookInfo.webhookURL(), "webhookFormat", webhookInfo.webhookFormat(), + "playerFormat", webhookInfo.playerFormat(), "enabled", webhookInfo.enabled()); + } + }; + private static final ConfigProcessor intSetProcessor = ConfigProcessor.INTEGER.collectionProcessor(IntOpenHashSet::new); private static final ConfigProcessor equipmentGroupInfo = new ConfigProcessor<>() { @@ -250,6 +288,53 @@ public RoundInfo dataFromElement(@NotNull ConfigElement element) throws ConfigPr } }; private static final ConfigProcessor> integerList = ConfigProcessor.INTEGER.listProcessor(); + + private static final ConfigProcessor playerCoinsInfo = new ConfigProcessor<>() { + @Override + public PlayerCoinsInfo dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { + String transactionMessageFormat = element.getStringOrThrow("transactionMessageFormat"); + String transactionDisplayFormat = element.getStringOrThrow("transactionDisplayFormat"); + TextColor gradientFrom = TextColor.color( + ConfigProcessors.rgbLike().dataFromElement(element.getElementOrThrow("gradientFrom"))); + TextColor gradientTo = TextColor.color( + ConfigProcessors.rgbLike().dataFromElement(element.getElementOrThrow("gradientTo"))); + long actionBarDuration = element.getNumberOrThrow("actionBarDuration").longValue(); + + return new PlayerCoinsInfo(transactionMessageFormat, transactionDisplayFormat, gradientFrom, gradientTo, + actionBarDuration); + } + + @Override + public @NotNull ConfigElement elementFromData(PlayerCoinsInfo playerCoinsInfo) throws ConfigProcessException { + return ConfigNode.of("transactionMessageFormat", playerCoinsInfo.transactionMessageFormat(), + "transactionDisplayFormat", playerCoinsInfo.transactionDisplayFormat(), "gradientFrom", + ConfigProcessors.rgbLike().elementFromData(playerCoinsInfo.gradientFrom()), "gradientTo", + ConfigProcessors.rgbLike().elementFromData(playerCoinsInfo.gradientTo()), "actionBarDuration", + playerCoinsInfo.actionBarDuration()); + } + }; + + private static final ConfigProcessor chatDestination = + ConfigProcessor.enumProcessor(ChatDestination.class); + + private static final ConfigProcessor messageWithDestination = new ConfigProcessor<>() { + @Override + public MessageWithDestination dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { + Component message = ConfigProcessors.component().dataFromElement(element.getElementOrThrow("component")); + ChatDestination destination = chatDestination.dataFromElement(element.getElementOrThrow("destination")); + return new MessageWithDestination(message, destination); + } + + @Override + public @NotNull ConfigElement elementFromData(MessageWithDestination messageWithDestination) { + return ConfigNode.of("component", messageWithDestination.component(), "destination", + messageWithDestination.destination()); + } + }; + + private static final ConfigProcessor>> messageWithDestinationListList = + messageWithDestination.listProcessor().listProcessor(); + private static final ConfigProcessor mapInfo = new ConfigProcessor<>() { @Override public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { @@ -272,12 +357,19 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co Component displayName = ConfigProcessors.component().dataFromElement(element.getElementOrThrow("displayName")); String displayItemTag = element.getStringOrThrow("displayItemSnbt"); - List introMessages = componentList.dataFromElement(element.getElementOrThrow("introMessages")); + long idleRevertTicks = element.getNumberOrThrow("idleRevertTicks").longValue(); + List> introMessages = + messageWithDestinationListList.dataFromElement(element.getElementOrThrow("introMessages")); + long countdownTicks = element.getNumberOrThrow("countdownTicks").longValue(); + List countdownAlertTicks = ConfigProcessor.LONG.listProcessor() + .dataFromElement(element.getElementOrThrow("countdownAlertTicks")); + Sound countdownTickSound = + ConfigProcessors.sound().dataFromElement(element.getElementOrThrow("countdownTickSound")); + String countdownTimeFormat = element.getStringOrThrow("countdownTimeFormat"); + long endTicks = element.getNumberOrThrow("endTicks").longValue(); + String endGameStatsFormat = element.getStringOrThrow("endGameStatsFormat"); Component scoreboardHeader = ConfigProcessors.component().dataFromElement(element.getElementOrThrow("scoreboardHeader")); - Vec3I leaderboardPosition = - VectorConfigProcessors.vec3I().dataFromElement(element.getElementOrThrow("leaderboardPosition")); - int leaderboardLength = element.getNumberOrThrow("leaderboardLength").intValue(); int worldTime = element.getNumberOrThrow("worldTime").intValue(); int maxPlayers = element.getNumberOrThrow("maxPlayers").intValue(); int minPlayers = element.getNumberOrThrow("minPlayers").intValue(); @@ -290,21 +382,19 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co long healTicks = element.getNumberOrThrow("healTicks").longValue(); double reviveRadius = element.getNumberOrThrow("reviveRadius").doubleValue(); boolean canWallshoot = element.getBooleanOrThrow("canWallshoot"); - boolean perksLostOnDeath = element.getBooleanOrThrow("perksLostOnDeath"); + List lostOnDeath = keyList.dataFromElement(element.getElementOrThrow("lostOnDeath")); long baseReviveTicks = element.getNumberOrThrow("baseReviveTicks").longValue(); int rollsPerChest = element.getNumberOrThrow("rollsPerChest").intValue(); float punchDamage = element.getNumberOrThrow("punchDamage").floatValue(); float punchRange = element.getNumberOrThrow("punchRange").floatValue(); + float punchKnockback = element.getNumberOrThrow("punchKnockback").floatValue(); + int punchCooldown = element.getNumberOrThrow("punchCooldown").intValue(); boolean mobPlayerCollisions = element.getBooleanOrThrow("mobPlayerCollisions"); - List milestoneRounds = integerList.dataFromElement(element.getElementOrThrow("milestoneRounds")); Map> defaultEquipment = keyToListKeyMap.dataFromElement(element.getElementOrThrow("defaultEquipment")); Map equipmentGroups = keyToEquipmentGroup.dataFromElement(element.getElementOrThrow("equipmentGroups")); - Sound countdownTickSound = - ConfigProcessors.sound().dataFromElement(element.getElementOrThrow("countdownTickSound")); - String countdownTimeFormat = element.getStringOrThrow("countdownTimeFormat"); String winTitleFormat = element.getStringOrThrow("winTitleFormat"); String winSubtitleFormat = element.getStringOrThrow("winSubtitleFormat"); String lossTitleFormat = element.getStringOrThrow("lossTitleFormat"); @@ -312,26 +402,47 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co String reviveStatusToReviverFormat = element.getStringOrThrow("reviveStatusToReviverFormat"); String reviveStatusToKnockedFormat = element.getStringOrThrow("reviveStatusToKnockedFormat"); String dyingStatusFormat = element.getStringOrThrow("dyingStatusFormat"); + String reviveMessageToRevivedFormat = element.getStringOrThrow("reviveMessageToRevivedFormat"); + String reviveMessageToOthersFormat = element.getStringOrThrow("reviveMessageToOthersFormat"); + Sound reviveSound = ConfigProcessors.sound().dataFromElement(element.getElementOrThrow("reviveSound")); String knockedMessageToKnockedFormat = element.getStringOrThrow("knockedMessageToKnockedFormat"); String knockedMessageToOthersFormat = element.getStringOrThrow("knockedMessageToOthersFormat"); String knockedTitleFormat = element.getStringOrThrow("knockedTitleFormat"); String knockedSubtitleFormat = element.getStringOrThrow("knockedSubtitleFormat"); + Sound knockedSound = ConfigProcessors.sound().dataFromElement(element.getElementOrThrow("knockedSound")); String deathMessageToKilledFormat = element.getStringOrThrow("deathMessageToKilledFormat"); String deathMessageToOthersFormat = element.getStringOrThrow("deathMessageToOthersFormat"); + Sound deathSound = ConfigProcessors.sound().dataFromElement(element.getElementOrThrow("deathSound")); String rejoinMessageFormat = element.getStringOrThrow("rejoinMessageFormat"); String quitMessageFormat = element.getStringOrThrow("quitMessageFormat"); - String endGameStatsFormat = element.getStringOrThrow("endGameStatsFormat"); + Component nearWindowMessage = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("nearWindowMessage")); + Component startRepairingMessage = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("startRepairingMessage")); + Component stopRepairingMessage = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("stopRepairingMessage")); + Component finishRepairingMessage = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("finishRepairingMessage")); + Component enemiesNearbyMessage = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("enemiesNearbyMessage")); + Component healthDisplay = + ConfigProcessors.component().dataFromElement(element.getElementOrThrow("healthDisplay")); + String gameJoinFormat = element.getStringOrThrow("gameJoinFormat"); + return new MapSettingsInfo(mapDataVersion, chunkLoadRange, id, instancePath, origin, minimumProtocolVersion, - maximumProtocolVersion, spawn, pitch, yaw, displayName, displayItemTag, introMessages, - scoreboardHeader, leaderboardPosition, leaderboardLength, worldTime, maxPlayers, minPlayers, - startingCoins, repairCoins, windowRepairRadius, powerupPickupRadius, windowRepairTicks, - corpseDeathTicks, healTicks, reviveRadius, canWallshoot, perksLostOnDeath, baseReviveTicks, - rollsPerChest, punchDamage, punchRange, mobPlayerCollisions, milestoneRounds, defaultEquipment, - equipmentGroups, countdownTickSound, countdownTimeFormat, winTitleFormat, winSubtitleFormat, - lossTitleFormat, lossSubtitleFormat, reviveStatusToReviverFormat, reviveStatusToKnockedFormat, - dyingStatusFormat, knockedMessageToKnockedFormat, knockedMessageToOthersFormat, knockedTitleFormat, - knockedSubtitleFormat, deathMessageToKilledFormat, deathMessageToOthersFormat, rejoinMessageFormat, - quitMessageFormat, endGameStatsFormat); + maximumProtocolVersion, spawn, pitch, yaw, displayName, displayItemTag, idleRevertTicks, + introMessages, countdownTicks, countdownAlertTicks, countdownTickSound, countdownTimeFormat, + endTicks, endGameStatsFormat, scoreboardHeader, worldTime, maxPlayers, minPlayers, startingCoins, + repairCoins, windowRepairRadius, powerupPickupRadius, windowRepairTicks, corpseDeathTicks, + healTicks, reviveRadius, canWallshoot, lostOnDeath, baseReviveTicks, rollsPerChest, punchDamage, + punchRange, punchKnockback, punchCooldown, mobPlayerCollisions, defaultEquipment, equipmentGroups, + winTitleFormat, winSubtitleFormat, lossTitleFormat, lossSubtitleFormat, reviveStatusToReviverFormat, + reviveStatusToKnockedFormat, dyingStatusFormat, reviveMessageToRevivedFormat, + reviveMessageToOthersFormat, reviveSound, knockedMessageToKnockedFormat, + knockedMessageToOthersFormat, knockedTitleFormat, knockedSubtitleFormat, knockedSound, + deathMessageToKilledFormat, deathMessageToOthersFormat, deathSound, rejoinMessageFormat, + quitMessageFormat, nearWindowMessage, startRepairingMessage, stopRepairingMessage, + finishRepairingMessage, enemiesNearbyMessage, healthDisplay, gameJoinFormat); } @Override @@ -349,11 +460,16 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co node.putNumber("yaw", mapConfig.yaw()); node.put("displayName", ConfigProcessors.component().elementFromData(mapConfig.displayName())); node.putString("displayItemSnbt", mapConfig.displayItemSnbt()); - node.put("introMessages", componentList.elementFromData(mapConfig.introMessages())); + node.putNumber("idleRevertTicks", mapConfig.idleRevertTicks()); + node.put("introMessages", messageWithDestinationListList.elementFromData(mapConfig.introMessages())); + node.putNumber("countdownTicks", mapConfig.countdownTicks()); + node.put("countdownAlertTicks", + ConfigProcessor.LONG.listProcessor().elementFromData(mapConfig.countdownAlertTicks())); + node.put("countdownTickSound", ConfigProcessors.sound().elementFromData(mapConfig.countdownTickSound())); + node.putString("countdownTimeFormat", mapConfig.countdownTimeFormat()); + node.putNumber("endTicks", mapConfig.endTicks()); + node.putString("endGameStatsFormat", mapConfig.endGameStatsFormat()); node.put("scoreboardHeader", ConfigProcessors.component().elementFromData(mapConfig.scoreboardHeader())); - node.put("leaderboardPosition", - VectorConfigProcessors.vec3I().elementFromData(mapConfig.leaderboardPosition())); - node.putNumber("leaderboardLength", mapConfig.leaderboardLength()); node.putNumber("worldTime", mapConfig.worldTime()); node.putNumber("maxPlayers", mapConfig.maxPlayers()); node.putNumber("minPlayers", mapConfig.minPlayers()); @@ -366,17 +482,16 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co node.putNumber("healTicks", mapConfig.healTicks()); node.putNumber("reviveRadius", mapConfig.reviveRadius()); node.putBoolean("canWallshoot", mapConfig.canWallshoot()); - node.putBoolean("perksLostOnDeath", mapConfig.perksLostOnDeath()); + node.put("lostOnDeath", keyList.elementFromData(mapConfig.lostOnDeath())); node.putNumber("baseReviveTicks", mapConfig.baseReviveTicks()); node.putNumber("rollsPerChest", mapConfig.rollsPerChest()); node.putNumber("punchDamage", mapConfig.punchDamage()); node.putNumber("punchRange", mapConfig.punchRange()); + node.putNumber("punchKnockback", mapConfig.punchKnockback()); + node.putNumber("punchCooldown", mapConfig.punchCooldown()); node.putBoolean("mobPlayerCollisions", mapConfig.mobPlayerCollisions()); - node.put("milestoneRounds", integerList.elementFromData(mapConfig.milestoneRounds())); node.put("defaultEquipment", keyToListKeyMap.elementFromData(mapConfig.defaultEquipment())); node.put("equipmentGroups", keyToEquipmentGroup.elementFromData(mapConfig.equipmentGroups())); - node.put("countdownTickSound", ConfigProcessors.sound().elementFromData(mapConfig.countdownTickSound())); - node.putString("countdownTimeFormat", mapConfig.countdownTimeFormat()); node.putString("winTitleFormat", mapConfig.winTitleFormat()); node.putString("winSubtitleFormat", mapConfig.winSubtitleFormat()); node.putString("lossTitleFormat", mapConfig.lossTitleFormat()); @@ -384,19 +499,33 @@ public MapSettingsInfo dataFromElement(@NotNull ConfigElement element) throws Co node.putString("reviveStatusToReviverFormat", mapConfig.reviveStatusToReviverFormat()); node.putString("reviveStatusToKnockedFormat", mapConfig.reviveStatusToKnockedFormat()); node.putString("dyingStatusFormat", mapConfig.dyingStatusFormat()); + node.putString("reviveMessageToRevivedFormat", mapConfig.reviveMessageToRevivedFormat()); + node.putString("reviveMessageToOthersFormat", mapConfig.reviveMessageToOthersFormat()); + node.put("reviveSound", ConfigProcessors.sound().elementFromData(mapConfig.reviveSound())); node.putString("knockedMessageToKnockedFormat", mapConfig.knockedMessageToKnockedFormat()); node.putString("knockedMessageToOthersFormat", mapConfig.knockedMessageToOthersFormat()); node.putString("knockedTitleFormat", mapConfig.knockedTitleFormat()); node.putString("knockedSubtitleFormat", mapConfig.knockedTitleFormat()); + node.put("knockedSound", ConfigProcessors.sound().elementFromData(mapConfig.knockedSound())); node.putString("deathMessageToKilledFormat", mapConfig.deathMessageToKilledFormat()); node.putString("deathMessageToOthersFormat", mapConfig.deathMessageToOthersFormat()); + node.put("deathSound", ConfigProcessors.sound().elementFromData(mapConfig.deathSound())); node.putString("rejoinMessageFormat", mapConfig.rejoinMessageFormat()); node.putString("quitMessageFormat", mapConfig.quitMessageFormat()); - node.putString("endGameStatsFormat", mapConfig.endGameStatsFormat()); + node.put("nearWindowMessage", ConfigProcessors.component().elementFromData(mapConfig.nearWindowMessage())); + node.put("startRepairingMessage", + ConfigProcessors.component().elementFromData(mapConfig.startRepairingMessage())); + node.put("stopRepairingMessage", + ConfigProcessors.component().elementFromData(mapConfig.stopRepairingMessage())); + node.put("finishRepairingMessage", + ConfigProcessors.component().elementFromData(mapConfig.finishRepairingMessage())); + node.put("enemiesNearbyMessage", + ConfigProcessors.component().elementFromData(mapConfig.enemiesNearbyMessage())); + node.put("healthDisplay", ConfigProcessors.component().elementFromData(mapConfig.healthDisplay())); + node.putString("gameJoinFormat", mapConfig.gameJoinFormat()); return node; } }; - private static final ConfigProcessor> stringList = ConfigProcessor.STRING.listProcessor(); private static final ConfigProcessor windowInfo = new ConfigProcessor<>() { @Override public @NotNull WindowInfo dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { @@ -492,6 +621,10 @@ private MapProcessors() { return mapInfo; } + public static @NotNull ConfigProcessor playerCoinsInfo() { + return playerCoinsInfo; + } + /** * Returns the {@link ConfigProcessor} used for serializing/deserializing {@link RoomInfo} objects. * @@ -573,6 +706,14 @@ private MapProcessors() { return spawnInfo; } + public static @NotNull ConfigProcessor leaderboardInfo() { + return leaderboardInfo; + } + + public static @NotNull ConfigProcessor webhookInfo() { + return webhookInfo; + } + public static @NotNull ConfigProcessor hologramInfo() { return hologramInfo; } diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapSettingsInfo.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapSettingsInfo.java index 90347ef8f..d21859eac 100644 --- a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapSettingsInfo.java +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/MapSettingsInfo.java @@ -4,7 +4,9 @@ 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 org.jetbrains.annotations.NotNull; +import org.phantazm.commons.chat.MessageWithDestination; import java.util.*; @@ -23,10 +25,15 @@ public record MapSettingsInfo(int mapDataVersion, float yaw, @NotNull Component displayName, @NotNull String displayItemSnbt, - @NotNull List introMessages, + long idleRevertTicks, + @NotNull List> introMessages, + long countdownTicks, + @NotNull List countdownAlertTicks, + @NotNull Sound countdownTickSound, + @NotNull String countdownTimeFormat, + long endTicks, + @NotNull String endGameStatsFormat, @NotNull Component scoreboardHeader, - @NotNull Vec3I leaderboardPosition, - int leaderboardLength, int worldTime, int maxPlayers, int minPlayers, @@ -39,17 +46,16 @@ public record MapSettingsInfo(int mapDataVersion, long healTicks, double reviveRadius, boolean canWallshoot, - boolean perksLostOnDeath, + @NotNull List lostOnDeath, long baseReviveTicks, int rollsPerChest, float punchDamage, float punchRange, + float punchKnockback, + int punchCooldown, boolean mobPlayerCollisions, - @NotNull List milestoneRounds, @NotNull Map> defaultEquipment, @NotNull Map equipmentGroups, - @NotNull Sound countdownTickSound, - @NotNull String countdownTimeFormat, @NotNull String winTitleFormat, @NotNull String winSubtitleFormat, @NotNull String lossTitleFormat, @@ -57,15 +63,26 @@ public record MapSettingsInfo(int mapDataVersion, @NotNull String reviveStatusToReviverFormat, @NotNull String reviveStatusToKnockedFormat, @NotNull String dyingStatusFormat, + @NotNull String reviveMessageToRevivedFormat, + @NotNull String reviveMessageToOthersFormat, + @NotNull Sound reviveSound, @NotNull String knockedMessageToKnockedFormat, @NotNull String knockedMessageToOthersFormat, @NotNull String knockedTitleFormat, @NotNull String knockedSubtitleFormat, + @NotNull Sound knockedSound, @NotNull String deathMessageToKilledFormat, @NotNull String deathMessageToOthersFormat, + @NotNull Sound deathSound, @NotNull String rejoinMessageFormat, @NotNull String quitMessageFormat, - @NotNull String endGameStatsFormat) { + @NotNull Component nearWindowMessage, + @NotNull Component startRepairingMessage, + @NotNull Component stopRepairingMessage, + @NotNull Component finishRepairingMessage, + @NotNull Component enemiesNearbyMessage, + @NotNull Component healthDisplay, + @NotNull String gameJoinFormat) { public static final int MAP_DATA_VERSION = 1; @@ -84,8 +101,6 @@ public record MapSettingsInfo(int mapDataVersion, * @param displayItemSnbt the SNBT for the item used to represent this map * @param introMessages the messages that may be sent when the game starts * @param scoreboardHeader the component displayed at the top of the scoreboard - * @param leaderboardPosition the position of the leaderboard, relative to origin - * @param leaderboardLength the number of entries in the leaderboard * @param worldTime the time in ticks that the world should have * @param maxPlayers the maximum number of players this map can fit * @param minPlayers the minimum number of players this map needs to start @@ -95,11 +110,9 @@ public record MapSettingsInfo(int mapDataVersion, * @param windowRepairTicks the number of ticks between each consecutive "repair tick" * @param corpseDeathTicks the number of ticks it takes for a downed player to fully die if they are not revived * @param reviveRadius the maximum distance away players can be from a corpse and still revive it - * @param canWallshoot true if wallshooting is enabled, false otherwise - * @param perksLostOnDeath true if perks are lost on death, false otherwise + * @param lostOnDeath object groups that are deleted on death * @param baseReviveTicks the base number of ticks it takes to revive a player * @param rollsPerChest the number of rolls a lucky chest can have before it moves to another location - * @param milestoneRounds "special" rounds whose times are recorded and saved * @param defaultEquipment the initial equipment players receive when the game starts; the keys correspond to * the inventory object group they should be placed in */ @@ -111,8 +124,6 @@ public record MapSettingsInfo(int mapDataVersion, Objects.requireNonNull(displayItemSnbt, "displayItemSnbt"); Objects.requireNonNull(introMessages, "introMessages"); Objects.requireNonNull(scoreboardHeader, "scoreboardHeader"); - Objects.requireNonNull(leaderboardPosition, "leaderboardPosition"); - Objects.requireNonNull(milestoneRounds, "milestoneRounds"); Objects.requireNonNull(defaultEquipment, "defaultEquipment"); } @@ -125,10 +136,19 @@ public record MapSettingsInfo(int mapDataVersion, */ public MapSettingsInfo(@NotNull Key id, @NotNull Vec3I origin) { this(MAP_DATA_VERSION, 10, id, List.of(), origin, 47, -1, Vec3I.ORIGIN, 0, 0, Component.text(id.value()), - "{id:\"stone\",Count:1,tag:{Name:\"" + id.value() + "\"}}", new ArrayList<>(0), - Component.text(id.value()), Vec3I.ORIGIN, 15, 0, 4, 1, 0, 20, 3, 1, 20, 500, 20, 2, false, false, 30, 5, - 0, 4.5F, false, new ArrayList<>(0), new HashMap<>(0), new HashMap<>(), - Sound.sound(Key.key("minecraft:entity.wolf.howl"), Sound.Source.MASTER, 1.0F, 1.0F), "", "", "", "", - "", "", "", "", "", "", "", "", "", "", "", "", ""); + "{id:\"stone\",Count:1,tag:{Name:\"" + id.value() + "\"}}", 12000L, new ArrayList<>(0), 400L, + new ArrayList<>(0), Sound.sound(Key.key("minecraft:entity.wolf.howl"), Sound.Source.MASTER, 1.0F, 1.0F), + "", 200L, "", Component.text(id.value()), 0, 4, 1, 0, 20, 3, 1, 20, 500, 20, 2, false, + new ArrayList<>(), 30, 5, 0, 4.5F, 0.4F, 20, false, new HashMap<>(0), new HashMap<>(), "", "", "", "", + "", "", "", "", "", + Sound.sound(Key.key("minecraft:block.brewing_stand.brew"), Sound.Source.MASTER, 1.0F, 1.0F), "", "", "", + "", Sound.sound(Key.key("minecraft:entity.ender_dragon.growl"), Sound.Source.MASTER, 1.0F, 0.5F), "", + "", Sound.sound(Key.key("minecraft:entity.player.hurt"), Sound.Source.MASTER, 1.0F, 1.0F), "", "", + Component.text("Hold SNEAK to repair", NamedTextColor.GREEN), + Component.text("Started repairing. Keep holding SNEAK to continue.", NamedTextColor.GREEN), + Component.text("Stopped repairing.", NamedTextColor.RED), + Component.text("Fully repaired!", NamedTextColor.GREEN), + Component.text("You cannot repair that window while enemies are nearby!", NamedTextColor.RED), + Component.text("❤", NamedTextColor.RED), ""); } } diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/PlayerCoinsInfo.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/PlayerCoinsInfo.java new file mode 100644 index 000000000..c813bb0b5 --- /dev/null +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/PlayerCoinsInfo.java @@ -0,0 +1,17 @@ +package org.phantazm.zombies.map; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; + +public record PlayerCoinsInfo(@NotNull String transactionMessageFormat, + @NotNull String transactionDisplayFormat, + @NotNull TextColor gradientFrom, + @NotNull TextColor gradientTo, + long actionBarDuration) { + + public static final PlayerCoinsInfo DEFAULT = + new PlayerCoinsInfo(" coins)'>", "", + NamedTextColor.GOLD, NamedTextColor.WHITE, 20); + +} diff --git a/zombies-mapdata/src/main/java/org/phantazm/zombies/map/WebhookInfo.java b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/WebhookInfo.java new file mode 100644 index 000000000..e6e99b3fd --- /dev/null +++ b/zombies-mapdata/src/main/java/org/phantazm/zombies/map/WebhookInfo.java @@ -0,0 +1,10 @@ +package org.phantazm.zombies.map; + +import org.jetbrains.annotations.NotNull; + +public record WebhookInfo(@NotNull String webhookURL, @NotNull String webhookFormat, @NotNull String playerFormat, + boolean enabled) { + + public static final WebhookInfo DEFAULT = new WebhookInfo("", "", "", false); + +} diff --git a/zombies-mapeditor/build.gradle.kts b/zombies-mapeditor/build.gradle.kts index 37a2c6f9b..f6e4603f9 100644 --- a/zombies-mapeditor/build.gradle.kts +++ b/zombies-mapeditor/build.gradle.kts @@ -1,12 +1,12 @@ // https://youtrack.jetbrains.com/issue/KTIJ-19369/False-positive-can-t-be-called-in-this-context-by-implicit-recei @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("phantazm.java-library-conventions") + id("phantazm.java-conventions") alias(libs.plugins.fabric.loom) } -version = "1.3.0+1.19.4" +version = "1.4.0+1.19.4" base { archivesName.set("phantazm-zombies-mapeditor") @@ -32,11 +32,9 @@ repositories { maven("https://server.bbkr.space/artifactory/libs-release") } -val fabricApiVersion: String by project - dependencies { - minecraft(libs.minecraft) - mappings(libs.yarn.mappings) { + minecraft(libs.minecraft.oneNineteen) + mappings(libs.yarn.mappings.oneNineteen) { artifact { classifier = "v2" } @@ -44,7 +42,7 @@ dependencies { modImplementation(libs.fabric.loader) modImplementation(libs.libgui) - modImplementation(libs.fabric.api) + modImplementation(libs.fabric.api.oneNineteen) modImplementation(libs.renderer) implementation(projects.phantazmCommons) @@ -85,8 +83,6 @@ tasks.compileJava { tasks.jar { from("../LICENSE") { - rename { - "${it}_${base.archivesName.get()}" - } + rename { "${it}_${archiveBaseName.get()}" } } } diff --git a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/MapeditorClient.java b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/MapeditorClient.java index 19a88d6a8..647e2a454 100644 --- a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/MapeditorClient.java +++ b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/MapeditorClient.java @@ -3,7 +3,6 @@ import com.github.steanky.ethylene.codec.yaml.YamlCodec; import com.github.steanky.ethylene.core.ConfigCodec; import io.github.cottonmc.cotton.gui.client.CottonClientScreen; -import io.netty.buffer.Unpooled; import me.x150.renderer.event.RenderEvents; import me.x150.renderer.render.Renderer3d; import net.fabricmc.api.ClientModInitializer; @@ -13,7 +12,6 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.event.player.UseBlockCallback; -import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.fabricmc.loader.api.FabricLoader; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; @@ -23,27 +21,17 @@ import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; import net.minecraft.text.Text; import net.minecraft.util.Formatting; -import net.minecraft.util.Identifier; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.HitResult; import org.jetbrains.annotations.NotNull; import org.lwjgl.glfw.GLFW; -import org.phantazm.commons.Namespaces; -import org.phantazm.messaging.MessageChannels; -import org.phantazm.messaging.packet.Packet; -import org.phantazm.messaging.packet.PacketHandler; -import org.phantazm.messaging.packet.c2p.MapDataVersionQueryPacket; -import org.phantazm.messaging.packet.c2p.MapDataVersionResponsePacket; -import org.phantazm.messaging.serialization.PacketSerializer; -import org.phantazm.messaging.serialization.PacketSerializers; +import org.phantazm.messaging.packet.player.MapDataVersionQueryPacket; import org.phantazm.zombies.map.FileSystemMapLoader; import org.phantazm.zombies.map.MapSettingsInfo; -import org.phantazm.zombies.mapeditor.client.packet.PacketByteBufDataReader; -import org.phantazm.zombies.mapeditor.client.packet.PacketByteBufDataWriter; +import org.phantazm.zombies.mapeditor.client.packet.FabricPacketUtils; +import org.phantazm.zombies.mapeditor.client.packet.MapDataVersionResponsePacketWrapper; import org.phantazm.zombies.mapeditor.client.render.ObjectRenderer; import org.phantazm.zombies.mapeditor.client.ui.MainGui; import org.phantazm.zombies.mapeditor.client.ui.NewObjectGui; @@ -88,49 +76,22 @@ public void onInitializeClient() { new KeyBinding(TranslationKeys.KEY_MAPEDITOR_CREATE, InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_N, TranslationKeys.CATEGORY_MAPEDITOR_ALL)); - PacketSerializer clientToProxy = PacketSerializers.clientToProxySerializer( - () -> new PacketByteBufDataWriter(new PacketByteBuf(Unpooled.buffer())), - data -> new PacketByteBufDataReader(new PacketByteBuf(Unpooled.wrappedBuffer(data)))); - Identifier clientToProxyIdentifier = Identifier.of(Namespaces.PHANTAZM, MessageChannels.CLIENT_TO_PROXY); - if (clientToProxyIdentifier != null) { - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> { - byte[] data = clientToProxy.serializePacket(new MapDataVersionQueryPacket()); - sender.sendPacket(new CustomPayloadC2SPacket(clientToProxyIdentifier, - new PacketByteBuf(Unpooled.wrappedBuffer(data)))); - })); - - PacketHandler clientToProxyHandler = new PacketHandler<>(clientToProxy) { - @Override - protected void handlePacket(@NotNull PacketSender packetSender, @NotNull Packet packet) { - if (packet instanceof MapDataVersionResponsePacket responsePacket) { - ClientPlayerEntity player = MinecraftClient.getInstance().player; - - if (player != null) { - Text message; - if (responsePacket.version() == MapSettingsInfo.MAP_DATA_VERSION) { - message = Text.translatable(TranslationKeys.CHAT_MAPEDITOR_MAPDATA_VERSION_SYNC_SYNCED) - .formatted(Formatting.GREEN); - } - else { - message = Text.translatable(TranslationKeys.CHAT_MAPEDITOR_MAPDATA_VERSION_SYNC_NOT_SYNCED) - .formatted(Formatting.RED); - } - player.sendMessage(message); - } + ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> { + FabricPacketUtils.sendPacket(sender, new MapDataVersionQueryPacket()); + })); + ClientPlayNetworking.registerGlobalReceiver(MapDataVersionResponsePacketWrapper.TYPE, + (packet, player, responseSender) -> { + Text message; + if (packet.packet().version() == MapSettingsInfo.MAP_DATA_VERSION) { + message = Text.translatable(TranslationKeys.CHAT_MAPEDITOR_MAPDATA_VERSION_SYNC_SYNCED) + .formatted(Formatting.GREEN); } - } - - @Override - protected void sendToReceiver(@NotNull PacketSender packetSender, byte @NotNull [] data) { - packetSender.sendPacket(clientToProxyIdentifier, new PacketByteBuf(Unpooled.wrappedBuffer(data))); - } - }; - ClientPlayNetworking.registerGlobalReceiver(clientToProxyIdentifier, - (client, handler, buf, responseSender) -> { - clientToProxyHandler.handleData(responseSender, buf.getWrittenBytes()); - }); - } - + else { + message = Text.translatable(TranslationKeys.CHAT_MAPEDITOR_MAPDATA_VERSION_SYNC_NOT_SYNCED) + .formatted(Formatting.RED); + } + player.sendMessage(message); + }); ClientTickEvents.END_CLIENT_TICK.register(new ClientTickEvents.EndTick() { private BlockHitResult lastBlockLook; diff --git a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/FabricPacketUtils.java b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/FabricPacketUtils.java new file mode 100644 index 000000000..c87abb8b9 --- /dev/null +++ b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/FabricPacketUtils.java @@ -0,0 +1,39 @@ +package org.phantazm.zombies.mapeditor.client.packet; + +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.kyori.adventure.key.Key; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.Packet; +import org.phantazm.messaging.serialization.DataReader; + +import java.util.function.Function; + +public class FabricPacketUtils { + + private FabricPacketUtils() { + throw new UnsupportedOperationException(); + } + + private static Identifier identifierFromKey(Key key) { + return Identifier.of(key.namespace(), key.value()); + } + + public static void sendPacket(@NotNull PacketSender sender, @NotNull Packet packet) { + PacketByteBufDataWriter dataWriter = new PacketByteBufDataWriter(); + packet.write(dataWriter); + sender.sendPacket(identifierFromKey(packet.getId()), dataWriter.getBuf()); + } + + public static @NotNull PacketType createPacketType(@NotNull Key key, + @NotNull Function innerCreator, + @NotNull Function wrapperCreator) { + return PacketType.create(identifierFromKey(key), buf -> { + TInnerPacket innerPacket = innerCreator.apply(new PacketByteBufDataReader(buf)); + return wrapperCreator.apply(innerPacket); + }); + } + +} diff --git a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/MapDataVersionResponsePacketWrapper.java b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/MapDataVersionResponsePacketWrapper.java new file mode 100644 index 000000000..08211f7f7 --- /dev/null +++ b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/MapDataVersionResponsePacketWrapper.java @@ -0,0 +1,31 @@ +package org.phantazm.zombies.mapeditor.client.packet; + +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.PacketByteBuf; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.proxy.MapDataVersionResponsePacket; + +import java.util.Objects; + +public record MapDataVersionResponsePacketWrapper(@NotNull MapDataVersionResponsePacket packet) implements FabricPacket { + + public static final PacketType TYPE = + FabricPacketUtils.createPacketType(MapDataVersionResponsePacket.ID, MapDataVersionResponsePacket::read, + MapDataVersionResponsePacketWrapper::new); + + public MapDataVersionResponsePacketWrapper { + Objects.requireNonNull(packet, "packet"); + } + + @Override + public void write(PacketByteBuf buf) { + packet.write(new PacketByteBufDataWriter()); + } + + @Override + public PacketType getType() { + return TYPE; + } + +} diff --git a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/PacketByteBufDataWriter.java b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/PacketByteBufDataWriter.java index 5e91e6291..13900a299 100644 --- a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/PacketByteBufDataWriter.java +++ b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/packet/PacketByteBufDataWriter.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.mapeditor.client.packet; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.minecraft.network.PacketByteBuf; import org.jetbrains.annotations.NotNull; import org.phantazm.messaging.serialization.DataWriter; @@ -22,6 +23,10 @@ public PacketByteBufDataWriter(@NotNull PacketByteBuf packetByteBuf) { this.packetByteBuf = Objects.requireNonNull(packetByteBuf, "packetByteBuf"); } + public PacketByteBufDataWriter() { + this(PacketByteBufs.create()); + } + @Override public void writeByte(byte data) { packetByteBuf.writeByte(data); @@ -36,4 +41,8 @@ public void writeInt(int data) { public byte @NotNull [] toByteArray() { return packetByteBuf.array(); } + + public @NotNull PacketByteBuf getBuf() { + return packetByteBuf; + } } diff --git a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/ui/MainGui.java b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/ui/MainGui.java index 29dfe498d..c98a1dd79 100644 --- a/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/ui/MainGui.java +++ b/zombies-mapeditor/src/main/java/org/phantazm/zombies/mapeditor/client/ui/MainGui.java @@ -9,8 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Namespaces; -import org.phantazm.zombies.map.MapInfo; -import org.phantazm.zombies.map.MapSettingsInfo; +import org.phantazm.zombies.map.*; import org.phantazm.zombies.mapeditor.client.EditorSession; import org.phantazm.zombies.mapeditor.client.Identifiers; import org.phantazm.zombies.mapeditor.client.TextPredicates; @@ -104,9 +103,11 @@ public MainGui(@NotNull EditorSession session) { return; } - session.addMap(new MapInfo(new MapSettingsInfo(mapKey, session.getFirstSelection()), new ArrayList<>(), - new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), - new ArrayList<>(), new LinkedConfigNode(0), new LinkedConfigNode())); + session.addMap( + new MapInfo(new MapSettingsInfo(mapKey, session.getFirstSelection()), PlayerCoinsInfo.DEFAULT, + new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), + new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), LeaderboardInfo.DEFAULT, + new LinkedConfigNode(0), new LinkedConfigNode(), WebhookInfo.DEFAULT)); session.setCurrent(mapKey); refreshMainGui(session); diff --git a/zombies-timer/build.gradle.kts b/zombies-timer/build.gradle.kts new file mode 100644 index 000000000..a523b9130 --- /dev/null +++ b/zombies-timer/build.gradle.kts @@ -0,0 +1,62 @@ +// https://youtrack.jetbrains.com/issue/KTIJ-19369/False-positive-can-t-be-called-in-this-context-by-implicit-recei +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("phantazm.java-conventions") + + alias(libs.plugins.fabric.loom) +} + +version = "0.2.1+1.20.1" + +base { + archivesName.set("zombies-autosplits") +} + +repositories { + mavenCentral() + maven("https://maven.shedaniel.me/") + maven("https://maven.terraformersmc.com/releases/") +} + +dependencies { + minecraft(libs.minecraft.oneTwenty) + mappings(libs.yarn.mappings.oneTwenty) { + artifact { + classifier = "v2" + } + } + + modImplementation(libs.fabric.loader) + modImplementation(libs.fabric.api.oneTwenty) + modImplementation(libs.modmenu) + modImplementation(libs.cloth.config) + + implementation(projects.phantazmCommons) + implementation(projects.phantazmMessaging) + implementation(libs.ethylene.yaml) + + include(projects.phantazmCommons) + include(projects.phantazmMessaging) + include(libs.adventure.key) + include(libs.examination.api) + include(libs.examination.string) + include(libs.ethylene.core) + include(libs.ethylene.yaml) + include(libs.toolkit.collection) + include(libs.toolkit.function) + include(libs.snakeyaml) +} + +tasks.processResources { + inputs.property("version", project.version) + + filesMatching("fabric.mod.json") { + expand("version" to project.version) + } +} + +tasks.jar { + from("../LICENSE") { + rename { "${it}_${archiveBaseName.get()}" } + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/ZombiesAutoSplitsClient.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/ZombiesAutoSplitsClient.java new file mode 100644 index 000000000..5946c6144 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/ZombiesAutoSplitsClient.java @@ -0,0 +1,193 @@ +package org.phantazm.zombies.autosplits; + +import com.github.steanky.ethylene.codec.yaml.YamlCodec; +import com.github.steanky.ethylene.core.BasicConfigHandler; +import com.github.steanky.ethylene.core.ConfigCodec; +import com.github.steanky.ethylene.core.ConfigHandler; +import com.github.steanky.ethylene.core.loader.ConfigLoader; +import com.github.steanky.ethylene.core.loader.SyncFileConfigLoader; +import com.github.steanky.ethylene.core.processor.ConfigProcessor; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.glfw.GLFW; +import org.phantazm.zombies.autosplits.config.ZombiesAutoSplitsConfig; +import org.phantazm.zombies.autosplits.config.ZombiesAutoSplitsConfigProcessor; +import org.phantazm.zombies.autosplits.event.ClientSoundCallback; +import org.phantazm.zombies.autosplits.packet.RoundStartPacketWrapper; +import org.phantazm.zombies.autosplits.render.RenderTimeHandler; +import org.phantazm.zombies.autosplits.sound.AutoSplitSoundInterceptor; +import org.phantazm.zombies.autosplits.splitter.AutoSplitSplitter; +import org.phantazm.zombies.autosplits.splitter.CompositeSplitter; +import org.phantazm.zombies.autosplits.splitter.internal.InternalSplitter; +import org.phantazm.zombies.autosplits.splitter.socket.LiveSplitSocketSplitter; +import org.phantazm.zombies.autosplits.tick.KeyInputHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.concurrent.*; + +public class ZombiesAutoSplitsClient implements ClientModInitializer { + + public static final String MODID = "zombies-autosplits"; + + private static ZombiesAutoSplitsClient instance = null; + + private final KeyBinding autoSplitsKeybind = + new KeyBinding("Toggle AutoSplits", GLFW.GLFW_KEY_SEMICOLON, "Phantazm"); + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + private final Logger logger = LoggerFactory.getLogger(MODID); + + private final ConfigHandler configHandler = new BasicConfigHandler(); + + private final ConfigHandler.ConfigKey configKey = + new ConfigHandler.ConfigKey<>(ZombiesAutoSplitsConfig.class, MODID + "_config"); + + private final Collection splitters = new CopyOnWriteArrayList<>(); + + private RenderTimeHandler renderTimeHandler; + + private ZombiesAutoSplitsConfig config; + + private CompositeSplitter compositeSplitter; + + public static @NotNull ZombiesAutoSplitsClient getInstance() { + return instance; + } + + private CompletableFuture loadConfigFromFile() { + return configHandler.writeDefaults().thenCompose((unused) -> configHandler.loadData(configKey)); + } + + public @NotNull ZombiesAutoSplitsConfig getConfig() { + return config; + } + + public void setConfig(@NotNull ZombiesAutoSplitsConfig config) { + for (AutoSplitSplitter splitter : splitters) { + splitter.cancel(); + } + splitters.clear(); + + this.config = config; + + if (config.useLiveSplits()) { + splitters.add(new LiveSplitSocketSplitter(executor, config.host(), config.port())); + } + if (config.useInternal()) { + InternalSplitter internalSplitter = new InternalSplitter(); + splitters.add(internalSplitter); + renderTimeHandler.setSplitter(internalSplitter); + } + else { + renderTimeHandler.setSplitter(null); + } + } + + public void saveConfig() { + configHandler.writeData(configKey, config).whenComplete((ignored, throwable) -> { + if (throwable != null) { + logger.error("Failed to save config", throwable); + } + }); + } + + @Override + public void onInitializeClient() { + renderTimeHandler = new RenderTimeHandler(MinecraftClient.getInstance(), 0xFFFFFF); + + initConfig(); + + compositeSplitter = new CompositeSplitter(MinecraftClient.getInstance(), logger, splitters); + + initEvents(); + initKeyBindings(); + initPackets(); + + instance = this; + } + + private void initConfig() { + ConfigCodec codec = new YamlCodec(); + Path configPath = FabricLoader.getInstance().getConfigDir().resolve("zombies-autosplits"); + String configFileName; + if (codec.getPreferredExtensions().isEmpty()) { + configFileName = "config"; + } + else { + configFileName = "config." + codec.getPreferredExtension(); + } + + try { + Files.createDirectories(configPath); + } + catch (IOException e) { + logger.error("Failed to create config directory", e); + return; + } + + Path configFile = configPath.resolve(configFileName); + ZombiesAutoSplitsConfig defaultConfig = ZombiesAutoSplitsConfig.DEFAULT; + ConfigProcessor configProcessor = new ZombiesAutoSplitsConfigProcessor(); + ConfigLoader configLoader = + new SyncFileConfigLoader<>(configProcessor, defaultConfig, configFile, codec); + configHandler.registerLoader(configKey, configLoader); + + ZombiesAutoSplitsConfig loadedConfig = loadConfigFromFile().join(); + setConfig(loadedConfig); + } + + private void initEvents() { + AutoSplitSoundInterceptor soundInterceptor = + new AutoSplitSoundInterceptor(MinecraftClient.getInstance(), compositeSplitter); + ClientTickEvents.END_CLIENT_TICK.register(new KeyInputHandler(autoSplitsKeybind, compositeSplitter)); + HudRenderCallback.EVENT.register(renderTimeHandler); + ClientSoundCallback.EVENT.register(soundInterceptor); + ClientLifecycleEvents.CLIENT_STOPPING.register(unused -> shutdownExecutor()); + } + + private void shutdownExecutor() { + logger.info("Shutting down executor"); + executor.shutdown(); + try { + if (executor.awaitTermination(5L, TimeUnit.SECONDS)) { + return; + } + + logger.warn("Executor did not shut down in 5 seconds. Force shutting down"); + executor.shutdownNow(); + + if (!executor.awaitTermination(5L, TimeUnit.SECONDS)) { + logger.warn("Executor did not terminate"); + } + } + catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + private void initKeyBindings() { + KeyBindingHelper.registerKeyBinding(autoSplitsKeybind); + } + + private void initPackets() { + ClientPlayNetworking.registerGlobalReceiver(RoundStartPacketWrapper.TYPE, (packet, player, responseSender) -> { + compositeSplitter.split(); + }); + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfig.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfig.java new file mode 100644 index 000000000..9c12679b9 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfig.java @@ -0,0 +1,65 @@ +package org.phantazm.zombies.autosplits.config; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public record ZombiesAutoSplitsConfig(@NotNull String host, int port, boolean useLiveSplits, boolean useInternal) { + + public static final String DEFAULT_HOST = "localhost"; + + public static final int DEFAULT_PORT = 16834; + + public static final boolean DEFAULT_USE_LIVE_SPLITS = false; + + public static final boolean DEFAULT_USE_INTERNAL = true; + + public static final ZombiesAutoSplitsConfig DEFAULT = + new ZombiesAutoSplitsConfig(DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USE_LIVE_SPLITS, DEFAULT_USE_INTERNAL); + + public ZombiesAutoSplitsConfig { + Objects.requireNonNull(host, "host"); + } + + public @NotNull Builder toBuilder() { + return new Builder().setHost(host).setPort(port).setUseLiveSplits(useLiveSplits).setUseInternal(useInternal); + } + + public static class Builder { + + private String host = DEFAULT_HOST; + + private int port = DEFAULT_PORT; + + private boolean useLiveSplits = DEFAULT_USE_LIVE_SPLITS; + + private boolean useInternal = DEFAULT_USE_INTERNAL; + + public @NotNull Builder setHost(String host) { + Objects.requireNonNull(host, "host"); + this.host = host; + return this; + } + + public @NotNull Builder setPort(int port) { + this.port = port; + return this; + } + + public @NotNull Builder setUseLiveSplits(boolean useLiveSplits) { + this.useLiveSplits = useLiveSplits; + return this; + } + + public @NotNull Builder setUseInternal(boolean useInternal) { + this.useInternal = useInternal; + return this; + } + + public @NotNull ZombiesAutoSplitsConfig build() { + return new ZombiesAutoSplitsConfig(host, port, useLiveSplits, useInternal); + } + + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfigProcessor.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfigProcessor.java new file mode 100644 index 000000000..f73ba624e --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/config/ZombiesAutoSplitsConfigProcessor.java @@ -0,0 +1,27 @@ +package org.phantazm.zombies.autosplits.config; + +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.collection.ConfigNode; +import com.github.steanky.ethylene.core.processor.ConfigProcessException; +import com.github.steanky.ethylene.core.processor.ConfigProcessor; +import org.jetbrains.annotations.NotNull; + +public class ZombiesAutoSplitsConfigProcessor implements ConfigProcessor { + @Override + public @NotNull ZombiesAutoSplitsConfig dataFromElement(@NotNull ConfigElement element) throws ConfigProcessException { + String host = element.getStringOrThrow("host"); + int port = element.getNumberOrThrow("port").intValue(); + if (port < 1 || port > 65535) { + throw new ConfigProcessException("Invalid port"); + } + boolean useLiveSplits = element.getBooleanOrThrow("useLiveSplits"); + boolean useInternal = element.getBooleanOrThrow("useInternal"); + + return new ZombiesAutoSplitsConfig(host, port, useLiveSplits, useInternal); + } + + @Override + public @NotNull ConfigElement elementFromData(@NotNull ZombiesAutoSplitsConfig config) { + return ConfigNode.of("host", config.host(), "port", config.port(), "useLiveSplits", config.useLiveSplits(), "useInternal", config.useInternal()); + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/event/ClientSoundCallback.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/event/ClientSoundCallback.java new file mode 100644 index 000000000..c233c997f --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/event/ClientSoundCallback.java @@ -0,0 +1,19 @@ +package org.phantazm.zombies.autosplits.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.client.sound.SoundInstance; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface ClientSoundCallback { + + Event EVENT = EventFactory.createArrayBacked(ClientSoundCallback.class, callbacks -> (sound) -> { + for (ClientSoundCallback callback : callbacks) { + callback.onPlaySound(sound); + } + }); + + void onPlaySound(@NotNull SoundInstance sound); + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/mixin/SoundSystemMixin.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/mixin/SoundSystemMixin.java new file mode 100644 index 000000000..24f473fe9 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/mixin/SoundSystemMixin.java @@ -0,0 +1,22 @@ +package org.phantazm.zombies.autosplits.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.sound.SoundInstance; +import net.minecraft.client.sound.SoundSystem; +import org.phantazm.zombies.autosplits.event.ClientSoundCallback; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Environment(EnvType.CLIENT) +@Mixin(SoundSystem.class) +abstract class SoundSystemMixin { + + @Inject(at = @At("HEAD"), method = "play(Lnet/minecraft/client/sound/SoundInstance;)V") + private void play(SoundInstance sound, CallbackInfo callbackInfo) { + ClientSoundCallback.EVENT.invoker().onPlaySound(sound); + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/modmenu/ZombiesAutoSplitsModMenuApiImpl.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/modmenu/ZombiesAutoSplitsModMenuApiImpl.java new file mode 100644 index 000000000..b3a7e614f --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/modmenu/ZombiesAutoSplitsModMenuApiImpl.java @@ -0,0 +1,60 @@ +package org.phantazm.zombies.autosplits.modmenu; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.text.Text; +import org.phantazm.zombies.autosplits.ZombiesAutoSplitsClient; +import org.phantazm.zombies.autosplits.config.ZombiesAutoSplitsConfig; + +public class ZombiesAutoSplitsModMenuApiImpl implements ModMenuApi { + + private final ConfigScreenFactory screenFactory = screen -> { + ZombiesAutoSplitsClient autoSplits = ZombiesAutoSplitsClient.getInstance(); + ZombiesAutoSplitsConfig autoSplitsConfig = autoSplits.getConfig(); + ZombiesAutoSplitsConfig.Builder autoSplitsConfigBuilder = autoSplitsConfig.toBuilder(); + + ConfigBuilder configBuilder = ConfigBuilder.create() + .setParentScreen(screen) + .setTitle(Text.of("Zombies AutoSplits Config")); + + ConfigEntryBuilder entryBuilder = configBuilder.entryBuilder(); + ConfigCategory main = configBuilder.getOrCreateCategory(Text.of("Config")); + main.addEntry(entryBuilder.startStrField(Text.of("Host"), autoSplitsConfig.host()) + .setDefaultValue(ZombiesAutoSplitsConfig.DEFAULT_HOST) + .setTooltip(Text.of("The host of the LiveSplits server. Most likely localhost.")) + .setSaveConsumer(autoSplitsConfigBuilder::setHost) + .build()); + main.addEntry(entryBuilder.startIntField(Text.of("Port"), autoSplitsConfig.port()) + .setDefaultValue(ZombiesAutoSplitsConfig.DEFAULT_PORT) + .setMin(1) + .setMax(65535) + .setTooltip(Text.of("The port of the LiveSplits server. Use -1 for the internal splitter.")) + .setSaveConsumer(autoSplitsConfigBuilder::setPort) + .build()); + main.addEntry(entryBuilder.startBooleanToggle(Text.of("Use LiveSplits"), autoSplitsConfig.useLiveSplits()) + .setDefaultValue(ZombiesAutoSplitsConfig.DEFAULT_USE_LIVE_SPLITS) + .setTooltip(Text.of("Whether to use the LiveSplits splitter.")) + .setSaveConsumer(autoSplitsConfigBuilder::setUseLiveSplits) + .build()); + main.addEntry(entryBuilder.startBooleanToggle(Text.of("Use Internal"), autoSplitsConfig.useInternal()) + .setDefaultValue(ZombiesAutoSplitsConfig.DEFAULT_USE_INTERNAL) + .setTooltip(Text.of("Whether to use the internal splitter.")) + .setSaveConsumer(autoSplitsConfigBuilder::setUseInternal) + .build()); + + configBuilder.setSavingRunnable(() -> { + autoSplits.setConfig(autoSplitsConfigBuilder.build()); + autoSplits.saveConfig(); + }); + + return configBuilder.build(); + }; + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return screenFactory; + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/FabricPacketUtils.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/FabricPacketUtils.java new file mode 100644 index 000000000..dabe1f8f7 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/FabricPacketUtils.java @@ -0,0 +1,39 @@ +package org.phantazm.zombies.autosplits.packet; + +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.kyori.adventure.key.Key; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.Packet; +import org.phantazm.messaging.serialization.DataReader; + +import java.util.function.Function; + +public class FabricPacketUtils { + + private FabricPacketUtils() { + throw new UnsupportedOperationException(); + } + + private static Identifier identifierFromKey(Key key) { + return Identifier.of(key.namespace(), key.value()); + } + + public static void sendPacket(@NotNull PacketSender sender, @NotNull Packet packet) { + PacketByteBufDataWriter dataWriter = new PacketByteBufDataWriter(); + packet.write(dataWriter); + sender.sendPacket(identifierFromKey(packet.getId()), dataWriter.getBuf()); + } + + public static @NotNull PacketType createPacketType(@NotNull Key key, + @NotNull Function innerCreator, + @NotNull Function wrapperCreator) { + return PacketType.create(identifierFromKey(key), buf -> { + TInnerPacket innerPacket = innerCreator.apply(new PacketByteBufDataReader(buf)); + return wrapperCreator.apply(innerPacket); + }); + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataReader.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataReader.java new file mode 100644 index 000000000..7d0cdf69b --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataReader.java @@ -0,0 +1,34 @@ +package org.phantazm.zombies.autosplits.packet; + +import net.minecraft.network.PacketByteBuf; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.serialization.DataReader; + +import java.util.Objects; + +/** + * A {@link DataReader} that reads from a {@link PacketByteBuf}. + */ +public class PacketByteBufDataReader implements DataReader { + + private final PacketByteBuf packetByteBuf; + + /** + * Creates a {@link PacketByteBufDataReader}. + * + * @param packetByteBuf The {@link PacketByteBuf} to read from + */ + public PacketByteBufDataReader(@NotNull PacketByteBuf packetByteBuf) { + this.packetByteBuf = Objects.requireNonNull(packetByteBuf, "packetByteBuf"); + } + + @Override + public byte readByte() { + return packetByteBuf.readByte(); + } + + @Override + public int readInt() { + return packetByteBuf.readInt(); + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataWriter.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataWriter.java new file mode 100644 index 000000000..5e7ecc4f5 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/PacketByteBufDataWriter.java @@ -0,0 +1,48 @@ +package org.phantazm.zombies.autosplits.packet; + +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.minecraft.network.PacketByteBuf; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.serialization.DataWriter; + +import java.util.Objects; + +/** + * A {@link DataWriter} that writes to a {@link PacketByteBuf}. + */ +public class PacketByteBufDataWriter implements DataWriter { + + private final PacketByteBuf packetByteBuf; + + /** + * Creates a {@link PacketByteBufDataWriter}. + * + * @param packetByteBuf The {@link PacketByteBuf} to write to + */ + public PacketByteBufDataWriter(@NotNull PacketByteBuf packetByteBuf) { + this.packetByteBuf = Objects.requireNonNull(packetByteBuf, "packetByteBuf"); + } + + public PacketByteBufDataWriter() { + this(PacketByteBufs.create()); + } + + @Override + public void writeByte(byte data) { + packetByteBuf.writeByte(data); + } + + @Override + public void writeInt(int data) { + packetByteBuf.writeInt(data); + } + + @Override + public byte @NotNull [] toByteArray() { + return packetByteBuf.array(); + } + + public @NotNull PacketByteBuf getBuf() { + return packetByteBuf; + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/RoundStartPacketWrapper.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/RoundStartPacketWrapper.java new file mode 100644 index 000000000..4fa955f1c --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/packet/RoundStartPacketWrapper.java @@ -0,0 +1,31 @@ +package org.phantazm.zombies.autosplits.packet; + +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.PacketByteBuf; +import org.jetbrains.annotations.NotNull; +import org.phantazm.messaging.packet.server.RoundStartPacket; + +import java.util.Objects; + +public record RoundStartPacketWrapper(@NotNull RoundStartPacket packet) implements FabricPacket { + + public static final PacketType TYPE = + FabricPacketUtils.createPacketType(RoundStartPacket.ID, RoundStartPacket::read, + RoundStartPacketWrapper::new); + + public RoundStartPacketWrapper { + Objects.requireNonNull(packet, "packet"); + } + + @Override + public void write(PacketByteBuf buf) { + packet.write(new PacketByteBufDataWriter()); + } + + @Override + public PacketType getType() { + return TYPE; + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/render/RenderTimeHandler.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/render/RenderTimeHandler.java new file mode 100644 index 000000000..f85bbb386 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/render/RenderTimeHandler.java @@ -0,0 +1,55 @@ +package org.phantazm.zombies.autosplits.render; + +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.Window; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.zombies.autosplits.splitter.internal.InternalSplitter; + +import java.util.Objects; + +public class RenderTimeHandler implements HudRenderCallback { + + private final MinecraftClient client; + + private InternalSplitter internalSplitter; + + private final int color; + + public RenderTimeHandler(@NotNull MinecraftClient client, int color) { + this.client = Objects.requireNonNull(client, "client"); + this.color = color; + } + + @Override + public void onHudRender(DrawContext drawContext, float tickDelta) { + if (internalSplitter == null) { + return; + } + + String time = getTimeString(); + int width = client.textRenderer.getWidth(time); + Window window = client.getWindow(); + int screenWidth = window.getScaledWidth(); + int screenHeight = window.getScaledHeight(); + drawContext.getMatrices().push(); + drawContext.drawTextWithShadow(client.textRenderer, time, screenWidth - width, + screenHeight - client.textRenderer.fontHeight, color); + drawContext.getMatrices().pop(); + } + + private String getTimeString() { + long millis = internalSplitter.getMillis(); + long minutesPart = millis / 60000; + long secondsPart = (millis % 60000) / 1000; + long tenthSecondsPart = (millis % 1000) / 100; + return String.format("%d:%02d:%d", minutesPart, secondsPart, tenthSecondsPart); + } + + public void setSplitter(@Nullable InternalSplitter internalSplitter) { + this.internalSplitter = internalSplitter; + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/sound/AutoSplitSoundInterceptor.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/sound/AutoSplitSoundInterceptor.java new file mode 100644 index 000000000..560a35d88 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/sound/AutoSplitSoundInterceptor.java @@ -0,0 +1,45 @@ +package org.phantazm.zombies.autosplits.sound; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.SoundInstance; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.autosplits.event.ClientSoundCallback; +import org.phantazm.zombies.autosplits.splitter.CompositeSplitter; + +import java.util.Objects; + +public class AutoSplitSoundInterceptor implements ClientSoundCallback { + + private final MinecraftClient client; + + private final CompositeSplitter compositeSplitter; + + public AutoSplitSoundInterceptor(@NotNull MinecraftClient client, @NotNull CompositeSplitter compositeSplitter) { + this.client = Objects.requireNonNull(client, "client"); + this.compositeSplitter = Objects.requireNonNull(compositeSplitter, "compositeSplitter"); + } + + @Override + public void onPlaySound(@NotNull SoundInstance sound) { + if (isOnHypixel() && isRoundSound(sound)) { + compositeSplitter.split(); + } + } + + private boolean isOnHypixel() { + if (client.player == null) { + return false; + } + + String brand = client.player.getServerBrand(); + return brand != null && brand.contains("Hypixel"); + } + + private boolean isRoundSound(SoundInstance sound) { + Identifier identifier = sound.getId(); + return identifier.equals(SoundEvents.ENTITY_WITHER_SPAWN.getId()) || identifier.equals(SoundEvents.ENTITY_ENDER_DRAGON_DEATH.getId()); + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/AutoSplitSplitter.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/AutoSplitSplitter.java new file mode 100644 index 000000000..4caa74195 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/AutoSplitSplitter.java @@ -0,0 +1,13 @@ +package org.phantazm.zombies.autosplits.splitter; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public interface AutoSplitSplitter { + + @NotNull CompletableFuture startOrSplit(); + + void cancel(); + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/CompositeSplitter.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/CompositeSplitter.java new file mode 100644 index 000000000..894885e5d --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/CompositeSplitter.java @@ -0,0 +1,56 @@ +package org.phantazm.zombies.autosplits.splitter; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.Objects; + +public class CompositeSplitter { + + private final MinecraftClient client; + + private final Logger logger; + + private final Collection splitters; + + private boolean enabled = true; + + public CompositeSplitter(@NotNull MinecraftClient client, @NotNull Logger logger, + @NotNull Collection splitters) { + this.client = Objects.requireNonNull(client, "client"); + this.logger = Objects.requireNonNull(logger, "logger"); + this.splitters = Objects.requireNonNull(splitters, "splitters"); + } + + public void split() { + if (!enabled) { + return; + } + + for (AutoSplitSplitter splitter : splitters) { + splitter.startOrSplit().whenComplete((ignored, throwable) -> { + if (throwable != null) { + logger.warn("Failed to split", throwable); + client.execute(this::warnFail); + } + }); + } + } + + private void warnFail() { + if (client.player != null) { + MutableText message = Text.literal("Failed to split!").formatted(Formatting.RED); + client.player.sendMessage(message, false); + } + } + + public boolean toggle() { + return enabled = !enabled; + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/internal/InternalSplitter.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/internal/InternalSplitter.java new file mode 100644 index 000000000..a03dc9d83 --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/internal/InternalSplitter.java @@ -0,0 +1,29 @@ +package org.phantazm.zombies.autosplits.splitter.internal; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.autosplits.splitter.AutoSplitSplitter; + +import java.util.concurrent.CompletableFuture; + +public class InternalSplitter implements AutoSplitSplitter { + + private long splitTime = 0L; + + @Override + public @NotNull CompletableFuture startOrSplit() { + splitTime = System.currentTimeMillis(); + return CompletableFuture.completedFuture(null); + } + + public void cancel() { + splitTime = 0L; + } + + public long getMillis() { + if (splitTime == 0L) { + return 0L; + } + + return System.currentTimeMillis() - splitTime; + } +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/socket/LiveSplitSocketSplitter.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/socket/LiveSplitSocketSplitter.java new file mode 100644 index 000000000..2577faa4d --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/splitter/socket/LiveSplitSocketSplitter.java @@ -0,0 +1,53 @@ +package org.phantazm.zombies.autosplits.splitter.socket; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.autosplits.splitter.AutoSplitSplitter; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.Socket; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class LiveSplitSocketSplitter implements AutoSplitSplitter { + + private final Executor executor; + + private final String host; + + private final int port; + + public LiveSplitSocketSplitter(@NotNull Executor executor, @NotNull String host, int port) { + this.executor = Objects.requireNonNull(executor, "executor"); + this.host = Objects.requireNonNull(host, "host"); + this.port = port; + } + + @Override + public @NotNull CompletableFuture startOrSplit() { + return sendCommand("startorsplit"); + } + + @Override + public void cancel() { + + } + + @SuppressWarnings("SameParameterValue") + private CompletableFuture sendCommand(String command) { + return CompletableFuture.runAsync(() -> { + try (Socket socket = new Socket(host, port); + OutputStream outputStream = socket.getOutputStream(); + Writer writer = new OutputStreamWriter(outputStream)) { + writer.write(command + "\r\n"); + writer.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, executor); + } + +} diff --git a/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/tick/KeyInputHandler.java b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/tick/KeyInputHandler.java new file mode 100644 index 000000000..49f3579ac --- /dev/null +++ b/zombies-timer/src/main/java/org/phantazm/zombies/autosplits/tick/KeyInputHandler.java @@ -0,0 +1,54 @@ +package org.phantazm.zombies.autosplits.tick; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.autosplits.splitter.CompositeSplitter; + +import java.util.Objects; + +public class KeyInputHandler implements ClientTickEvents.EndTick { + + private final KeyBinding keyBinding; + + private final CompositeSplitter compositeSplitter; + + public KeyInputHandler(@NotNull KeyBinding keyBinding, @NotNull CompositeSplitter compositeSplitter) { + this.keyBinding = Objects.requireNonNull(keyBinding, "keyBinding"); + this.compositeSplitter = Objects.requireNonNull(compositeSplitter, "compositeSplitter"); + } + + @Override + public void onEndTick(MinecraftClient client) { + if (!keyBinding.wasPressed()) { + return; + } + + Text toggledComponent; + boolean toggled = compositeSplitter.toggle(); + + PlayerEntity player = client.player; + if (player == null) { + return; + } + + if (toggled) { + toggledComponent = Text.literal("ON").formatted(Formatting.GREEN); + } + else { + toggledComponent = Text.literal("OFF").formatted(Formatting.RED); + } + + Text message = Text.empty() + .formatted(Formatting.YELLOW) + .append("Toggled AutoSplits ") + .append(toggledComponent) + .append("!"); + client.player.sendMessage(message, false); + } + +} diff --git a/zombies-timer/src/main/resources/fabric.mod.json b/zombies-timer/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..db8b9b37e --- /dev/null +++ b/zombies-timer/src/main/resources/fabric.mod.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "id": "zombies-autosplits", + "version": "${version}", + "name": "Phantazm Timer Mod", + "description": "A timer mod for Phantazm.", + "authors": [ + "thamid" + ], + "contact": { + "issues": "https://github.com/PhantazmNetwork/PhantazmServer/issues", + "sources": "https://github.com/PhantazmNetwork/PhantazmServer.git" + }, + "license": "AGPL-3.0-or-later", + "environment": "client", + "entrypoints": { + "client": [ + "org.phantazm.zombies.autosplits.ZombiesAutoSplitsClient" + ], + "modmenu": [ + "org.phantazm.zombies.autosplits.modmenu.ZombiesAutoSplitsModMenuApiImpl" + ] + }, + "mixins": [ + "zombies-autosplits.mixin.json" + ], + "depends": { + "fabricloader": ">=0.14.21", + "fabric-api": "*", + "minecraft": "1.20.1", + "java": ">=17", + "cloth-config": "^11.1.106", + "modmenu": "^7.1.0" + } +} diff --git a/zombies-timer/src/main/resources/zombies-autosplits.mixin.json b/zombies-timer/src/main/resources/zombies-autosplits.mixin.json new file mode 100644 index 000000000..8eba4405f --- /dev/null +++ b/zombies-timer/src/main/resources/zombies-autosplits.mixin.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.phantazm.zombies.autosplits.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [], + "client": [ + "SoundSystemMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/zombies/build.gradle.kts b/zombies/build.gradle.kts index 6973d4926..38180f812 100644 --- a/zombies/build.gradle.kts +++ b/zombies/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(projects.phantazmCore) + api(projects.phantazmMessaging) api(projects.phantazmMob) api(projects.phantazmStats) api(projects.phantazmZombiesMapdata) diff --git a/zombies/src/main/java/org/phantazm/zombies/Attributes.java b/zombies/src/main/java/org/phantazm/zombies/Attributes.java index 83070d456..27d99f6db 100644 --- a/zombies/src/main/java/org/phantazm/zombies/Attributes.java +++ b/zombies/src/main/java/org/phantazm/zombies/Attributes.java @@ -9,7 +9,7 @@ private Attributes() { /** * Expansion applied to the hitbox of entities. Used to make things easier (or harder) to hit with bullets. */ - public static final Attribute HITBOX_EXPANSION = new Attribute("phantazm.hitbox_expand", 0.25F, 2048F); + public static final Attribute HITBOX_EXPANSION = new Attribute("phantazm.hitbox_expand", 0.35F, 2048F); /** * Multiplier applied to the fire rate when shooting guns. @@ -26,6 +26,22 @@ private Attributes() { */ public static final Attribute HEAL_TICKS = new Attribute("phantazm.heal_rate", 20F, 2048F); + /** + * Damage multiplier for the player. + */ + public static final Attribute DAMAGE_MULTIPLIER = new Attribute("phantazm.damage_multiplier", 1F, 2048F); + + /** + * Attack speed multiplier for mobs. + */ + public static final Attribute ATTACK_SPEED_MULTIPLIER = + new Attribute("phantazm.attack_speed_multiplier", 1F, 2048F); + + /** + * When applied to a mob, modifies any headshot damage received. + */ + public static final Attribute HEADSHOT_DAMAGE_MULTIPLIER = new Attribute("phantazm.headshot_multiplier", 1F, 2048F); + /** * The "nil" attribute. Used as a fallback when the desired attribute cannot be found. */ @@ -36,6 +52,9 @@ public static void registerAll() { FIRE_RATE_MULTIPLIER.register(); REVIVE_TICKS.register(); HEAL_TICKS.register(); + DAMAGE_MULTIPLIER.register(); + ATTACK_SPEED_MULTIPLIER.register(); + HEADSHOT_DAMAGE_MULTIPLIER.register(); NIL.register(); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/ExtraNodeKeys.java b/zombies/src/main/java/org/phantazm/zombies/ExtraNodeKeys.java index ba8b2495a..69d051757 100644 --- a/zombies/src/main/java/org/phantazm/zombies/ExtraNodeKeys.java +++ b/zombies/src/main/java/org/phantazm/zombies/ExtraNodeKeys.java @@ -5,4 +5,16 @@ private ExtraNodeKeys() { } public static final String RESIST_INSTAKILL = "resistInstakill"; + + public static final String RESIST_FIRE = "resistFire"; + + public static final String RESIST_SLOW_DOWN = "resistSlowDown"; + + public static final String TICKS_UNTIL_DEATH = "ticksUntilDeath"; + + public static final String SPEEDUP_INCREMENTS = "speedupIncrements"; + + public static final String SPEEDUP_INTERVAL = "speedupInterval"; + + public static final String SPEEDUP_AMOUNT = "speedupAmount"; } diff --git a/zombies/src/main/java/org/phantazm/zombies/Flags.java b/zombies/src/main/java/org/phantazm/zombies/Flags.java index f98cd1e32..9ba20f1b7 100644 --- a/zombies/src/main/java/org/phantazm/zombies/Flags.java +++ b/zombies/src/main/java/org/phantazm/zombies/Flags.java @@ -12,4 +12,7 @@ private Flags() { public static final Key GODMODE = Key.key(Namespaces.PHANTAZM, "zombies.player.flag.godmode"); public static final Key BOMBED_ROOM = Key.key(Namespaces.PHANTAZM, "zombies.map.room.flag.bombed"); + + public static final Key WALLSHOOTING_ENABLED = + Key.key(Namespaces.PHANTAZM, "zombies.map.flag.wallshooting_enabled"); } diff --git a/zombies/src/main/java/org/phantazm/zombies/Tags.java b/zombies/src/main/java/org/phantazm/zombies/Tags.java index 37b5b3b9b..fb249ff0a 100644 --- a/zombies/src/main/java/org/phantazm/zombies/Tags.java +++ b/zombies/src/main/java/org/phantazm/zombies/Tags.java @@ -1,5 +1,6 @@ package org.phantazm.zombies; +import net.kyori.adventure.text.Component; import net.minestom.server.tag.Tag; import java.util.UUID; @@ -12,7 +13,14 @@ private Tags() { public static final Tag LAST_HIT_BY = Tag.UUID("last_hit"); + public static final Tag PROJECTILE_SHOOTER = Tag.UUID("projectile_shooter"); + public static final Tag ARMOR_TIER = Tag.Integer("armor_tier").defaultValue(-1); - public static final Tag LAST_SHOP_ACTIVATE = Tag.Long("last_shop_activate").defaultValue(-1L); + public static final Tag LAST_ENTER_BOMBED_ROOM = Tag.Long("entered_bombed_room").defaultValue(-1L); + + public static final Tag SKILL_IDENTIFIER = Tag.UUID("skill_identifier"); + + public static final Tag DAMAGE_NAME = Tag.Component("damage_name"); + } diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/BasicPlayerCoins.java b/zombies/src/main/java/org/phantazm/zombies/coin/BasicPlayerCoins.java index fde324256..3d38eab27 100644 --- a/zombies/src/main/java/org/phantazm/zombies/coin/BasicPlayerCoins.java +++ b/zombies/src/main/java/org/phantazm/zombies/coin/BasicPlayerCoins.java @@ -2,59 +2,53 @@ import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.player.PlayerView; +import org.phantazm.commons.Tickable; import org.phantazm.stats.zombies.ZombiesPlayerMapStats; -import org.phantazm.zombies.coin.component.TransactionComponentCreator; +import org.phantazm.zombies.coin.component.TransactionMessager; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; -public class BasicPlayerCoins implements PlayerCoins { - private final PlayerView playerView; +public class BasicPlayerCoins implements PlayerCoins, Tickable { private final ZombiesPlayerMapStats stats; - private final TransactionComponentCreator componentCreator; - private volatile int coins; - private final Object sync; + private final TransactionMessager transactionMessager; + private int coins; - public BasicPlayerCoins(@NotNull PlayerView playerView, @NotNull ZombiesPlayerMapStats stats, - @NotNull TransactionComponentCreator componentCreator, int initialCoins) { - this.playerView = Objects.requireNonNull(playerView, "playerView"); + public BasicPlayerCoins(@NotNull ZombiesPlayerMapStats stats, @NotNull TransactionMessager transactionMessager, + int initialCoins) { this.stats = Objects.requireNonNull(stats, "stats"); - this.componentCreator = Objects.requireNonNull(componentCreator, "componentCreator"); + this.transactionMessager = Objects.requireNonNull(transactionMessager, "componentCreator"); this.coins = initialCoins; - - this.sync = new Object(); } @Override public @NotNull TransactionResult runTransaction(@NotNull Transaction transaction) { - synchronized (sync) { - List modifiers = new ArrayList<>(transaction.modifiers()); - modifiers.sort(Comparator.comparingInt(Transaction.Modifier::getPriority).reversed()); - - List modifierNames = new ArrayList<>(modifiers.size()); - int change = transaction.initialChange(); - for (Transaction.Modifier modifier : modifiers) { - int newChange = modifier.modify(change); - if (change != newChange) { - modifierNames.add(modifier.getDisplayName()); - } + List modifiers = new ArrayList<>(transaction.modifiers()); + modifiers.sort(Comparator.comparingInt(Transaction.Modifier::getPriority).reversed()); - change = newChange; + List displays = new ArrayList<>(modifiers.size() + transaction.extraDisplays().size()); + int change = transaction.initialChange(); + for (Transaction.Modifier modifier : modifiers) { + int newChange = modifier.modify(change); + if (change != newChange) { + displays.add(modifier.getDisplayName()); } - int newCoins = coins + change; - if (change < 0 && newCoins > coins) { - change = -(coins - Integer.MIN_VALUE); - } - else if (change > 0 && newCoins < coins) { - change = Integer.MAX_VALUE - coins; - } + change = newChange; + } + displays.addAll(transaction.extraDisplays()); - return new TransactionResult(modifierNames, change); + int newCoins = coins + change; + if (change < 0 && newCoins > coins) { + change = -(coins - Integer.MIN_VALUE); } + else if (change > 0 && newCoins < coins) { + change = Integer.MAX_VALUE - coins; + } + + return new TransactionResult(displays, change); } @Override @@ -64,30 +58,28 @@ public int getCoins() { @Override public void applyTransaction(@NotNull TransactionResult result) { - synchronized (sync) { - if (!result.hasChange()) { - return; - } - - if (result.change() > 0) { - stats.setCoinsGained(stats.getCoinsGained() + result.change()); - } else { - stats.setCoinsSpent(stats.getCoinsSpent() - result.change()); - } + if (!result.hasChange()) { + return; + } - coins += result.change(); - playerView.getPlayer().ifPresent(player -> { - Component message = - componentCreator.createTransactionComponent(result.modifierNames(), result.change()); - player.sendMessage(message); - }); + if (result.change() > 0) { + stats.setCoinsGained(stats.getCoinsGained() + result.change()); } + else { + stats.setCoinsSpent(stats.getCoinsSpent() - result.change()); + } + + coins += result.change(); + transactionMessager.sendMessage(result.displays(), result.change()); } @Override public void set(int newValue) { - synchronized (sync) { - coins = newValue; - } + coins = newValue; + } + + @Override + public void tick(long time) { + transactionMessager.tick(time); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/PlayerCoins.java b/zombies/src/main/java/org/phantazm/zombies/coin/PlayerCoins.java index 2b0b053f7..b4f606544 100644 --- a/zombies/src/main/java/org/phantazm/zombies/coin/PlayerCoins.java +++ b/zombies/src/main/java/org/phantazm/zombies/coin/PlayerCoins.java @@ -1,8 +1,9 @@ package org.phantazm.zombies.coin; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Tickable; -public interface PlayerCoins { +public interface PlayerCoins extends Tickable { @NotNull TransactionResult runTransaction(@NotNull Transaction transaction); default @NotNull TransactionResult modify(int change) { diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/Transaction.java b/zombies/src/main/java/org/phantazm/zombies/coin/Transaction.java index 46b3e6665..3bbbb7508 100644 --- a/zombies/src/main/java/org/phantazm/zombies/coin/Transaction.java +++ b/zombies/src/main/java/org/phantazm/zombies/coin/Transaction.java @@ -6,10 +6,15 @@ import java.util.Collection; import java.util.List; -public record Transaction(@NotNull Collection modifiers, int initialChange) { +public record Transaction(@NotNull Collection modifiers, @NotNull Collection extraDisplays, + int initialChange) { public Transaction(int initialChange) { - this(List.of(), initialChange); + this(List.of(), List.of(), initialChange); + } + + public Transaction(@NotNull Collection modifiers, int initialChange) { + this(modifiers, List.of(), initialChange); } public interface Modifier { diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/TransactionResult.java b/zombies/src/main/java/org/phantazm/zombies/coin/TransactionResult.java index 64e5d071d..a0b53d46e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/coin/TransactionResult.java +++ b/zombies/src/main/java/org/phantazm/zombies/coin/TransactionResult.java @@ -6,10 +6,10 @@ import java.util.List; import java.util.Objects; -public record TransactionResult(@NotNull List modifierNames, int change) { +public record TransactionResult(@NotNull List displays, int change) { public TransactionResult { - Objects.requireNonNull(modifierNames, "modifierNames"); + Objects.requireNonNull(displays, "modifierNames"); } public boolean applyIfAffordable(@NotNull PlayerCoins coins) { diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionComponentCreator.java b/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionComponentCreator.java deleted file mode 100644 index 863619a43..000000000 --- a/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionComponentCreator.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.phantazm.zombies.coin.component; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.format.NamedTextColor; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class BasicTransactionComponentCreator implements TransactionComponentCreator { - @Override - public @NotNull Component createTransactionComponent(@NotNull List modifierNames, int change) { - TextComponent.Builder builder = Component.text().color(NamedTextColor.GOLD); - if (change >= 0) { - builder.append(Component.text("+")); - } - - builder.append(Component.text(change)); - builder.append(Component.text(" coins")); - - if (!modifierNames.isEmpty()) { - builder.append(Component.text(" (")); - for (int i = 0; i < modifierNames.size(); i++) { - if (i > 0) { - builder.append(Component.text(", ")); - } - builder.append(modifierNames.get(i)); - } - builder.append(Component.text(")")); - } - - return builder.build(); - } -} diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionMessager.java b/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionMessager.java new file mode 100644 index 000000000..1091ed1e3 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/coin/component/BasicTransactionMessager.java @@ -0,0 +1,88 @@ +package org.phantazm.zombies.coin.component; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.MiniMessageUtils; +import org.phantazm.zombies.map.PlayerCoinsInfo; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class BasicTransactionMessager implements TransactionMessager { + + private final ZombiesPlayerActionBar actionBar; + + private final MiniMessage miniMessage; + + private final PlayerCoinsInfo coinsInfo; + + private Component lastMessage = null; + + private long lastMessageTick; + + private long ticks; + + public BasicTransactionMessager(@NotNull ZombiesPlayerActionBar actionBar, @NotNull MiniMessage miniMessage, + @NotNull PlayerCoinsInfo coinsInfo) { + this.actionBar = Objects.requireNonNull(actionBar, "actionBar"); + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + this.coinsInfo = Objects.requireNonNull(coinsInfo, "coinsInfo"); + } + + @Override + public void sendMessage(@NotNull List displays, int change) { + TagResolver positivePlaceholder = Formatter.choice("positive", change >= 0 ? 1 : 0); + TagResolver changePlaceholder = Placeholder.component("change", Component.text(Math.abs(change))); + TagResolver displaysPresentPlaceholder = MiniMessageUtils.optional("displays_present", !displays.isEmpty()); + Collection mappedDisplays = new ArrayList<>(displays.size()); + for (Component display : displays) { + mappedDisplays.add(miniMessage.deserialize(coinsInfo.transactionDisplayFormat(), + Placeholder.component("display", display))); + } + TagResolver displaysPlaceholder = MiniMessageUtils.list("displays", mappedDisplays); + + lastMessage = + miniMessage.deserialize(coinsInfo.transactionMessageFormat(), positivePlaceholder, changePlaceholder, + displaysPresentPlaceholder, displaysPlaceholder); + lastMessageTick = ticks; + } + + @Override + public void tick(long time) { + ++ticks; + + if (lastMessage != null) { + if (ticks - lastMessageTick > coinsInfo.actionBarDuration()) { + lastMessage = null; + } else { + float progress = (float)(ticks - lastMessageTick) / coinsInfo.actionBarDuration(); + actionBar.sendActionBar(lerpColorRecursive(lastMessage, progress).asComponent(), ZombiesPlayerActionBar.COINS_PRIORITY); + } + } + } + + private ComponentLike lerpColorRecursive(Component component, float progress) { + List children = new ArrayList<>(component.children().size()); + for (Component child : component.children()) { + children.add(lerpColorRecursive(child, progress)); + } + + TextColor toColor = component.color(); + if (toColor == null) { + toColor = coinsInfo.gradientTo(); + } + + TextColor newColor = TextColor.lerp(progress, coinsInfo.gradientFrom(), toColor); + return component.children(children).color(newColor); + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionComponentCreator.java b/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionComponentCreator.java deleted file mode 100644 index 25f6d847c..000000000 --- a/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionComponentCreator.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.phantazm.zombies.coin.component; - -import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public interface TransactionComponentCreator { - - @NotNull Component createTransactionComponent(@NotNull List modifierNames, int change); - -} diff --git a/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionMessager.java b/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionMessager.java new file mode 100644 index 000000000..0440cbd7b --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/coin/component/TransactionMessager.java @@ -0,0 +1,13 @@ +package org.phantazm.zombies.coin.component; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Tickable; + +import java.util.List; + +public interface TransactionMessager extends Tickable { + + void sendMessage(@NotNull List displays, int change); + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/command/RoundCommand.java b/zombies/src/main/java/org/phantazm/zombies/command/RoundCommand.java index 02785a139..7ccb271b4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/command/RoundCommand.java +++ b/zombies/src/main/java/org/phantazm/zombies/command/RoundCommand.java @@ -9,7 +9,6 @@ import net.minestom.server.entity.Player; import net.minestom.server.permission.Permission; import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.map.handler.RoundHandler; import org.phantazm.zombies.scene.ZombiesScene; import org.phantazm.zombies.stage.Stage; @@ -59,14 +58,12 @@ public RoundCommand(@NotNull Function> scen Stage current = transition.getCurrentStage(); if (current == null || !current.key().equals(StageKeys.IN_GAME)) { transition.setCurrentStage(StageKeys.IN_GAME); - } - - handler.currentRound().ifPresent(round -> { - for (PhantazmMob mob : round.getSpawnedMobs()) { - mob.entity().kill(); + if (roundIndex != 0) { + handler.setCurrentRound(roundIndex); } - }); - handler.setCurrentRound(roundIndex); + } else { + handler.setCurrentRound(roundIndex); + } }); }, roundArgument); } diff --git a/zombies/src/main/java/org/phantazm/zombies/command/ZombiesJoinCommand.java b/zombies/src/main/java/org/phantazm/zombies/command/ZombiesJoinCommand.java index 11910034c..f44b46444 100644 --- a/zombies/src/main/java/org/phantazm/zombies/command/ZombiesJoinCommand.java +++ b/zombies/src/main/java/org/phantazm/zombies/command/ZombiesJoinCommand.java @@ -28,6 +28,7 @@ public ZombiesJoinCommand(@NotNull Map partyMap, super("join"); Argument mapKeyArgument = ArgumentType.String("map-key"); + Argument restrictedArgument = ArgumentType.Boolean("restricted").setDefaultValue(false); Objects.requireNonNull(partyMap, "partyMap"); Objects.requireNonNull(viewProvider, "viewProvider"); @@ -89,8 +90,8 @@ public ZombiesJoinCommand(@NotNull Map partyMap, } } - joinHelper.joinGame(joiner, playerViews, targetMap); - }, mapKeyArgument); + joinHelper.joinGame(joiner, playerViews, targetMap, context.get(restrictedArgument)); + }, mapKeyArgument, restrictedArgument); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/command/ZombiesRejoinCommand.java b/zombies/src/main/java/org/phantazm/zombies/command/ZombiesRejoinCommand.java index 03e7c00d2..c19d6dd33 100644 --- a/zombies/src/main/java/org/phantazm/zombies/command/ZombiesRejoinCommand.java +++ b/zombies/src/main/java/org/phantazm/zombies/command/ZombiesRejoinCommand.java @@ -12,21 +12,23 @@ import org.phantazm.zombies.scene.ZombiesJoinHelper; import org.phantazm.zombies.scene.ZombiesScene; import org.phantazm.zombies.scene.ZombiesSceneRouter; +import org.phantazm.zombies.stage.Stage; import java.util.Objects; import java.util.Optional; import java.util.UUID; public class ZombiesRejoinCommand extends Command { - public ZombiesRejoinCommand(@NotNull ZombiesSceneRouter router, - @NotNull PlayerViewProvider viewProvider, @NotNull ZombiesJoinHelper joinHelper) { + public ZombiesRejoinCommand(@NotNull ZombiesSceneRouter router, @NotNull PlayerViewProvider viewProvider, + @NotNull ZombiesJoinHelper joinHelper) { super("rejoin"); Objects.requireNonNull(router, "router"); Objects.requireNonNull(viewProvider, "viewProvider"); Objects.requireNonNull(joinHelper, "joinHelper"); - Argument targetGameArgument = ArgumentType.UUID("target-game"); + Argument targetGameArgument = ArgumentType.UUID("target-game").setDefaultValue(() -> null); + targetGameArgument.setSuggestionCallback((sender, context, suggestion) -> { if (!(sender instanceof Player player)) { return; @@ -38,6 +40,11 @@ public ZombiesRejoinCommand(@NotNull ZombiesSceneRouter router, continue; } + Stage stage = scene.getCurrentStage(); + if (stage != null && !stage.canRejoin()) { + continue; + } + SuggestionEntry entry = new SuggestionEntry(scene.getUUID().toString(), scene.getMapSettingsInfo().displayName()); suggestion.addEntry(entry); @@ -59,22 +66,42 @@ public ZombiesRejoinCommand(@NotNull ZombiesSceneRouter router, UUID targetGame = context.get(targetGameArgument); Optional currentScene = router.getCurrentScene(sender.identity().uuid()); + if (targetGame == null) { + for (ZombiesScene scene : router.getScenesContainingPlayer(sender.identity().uuid())) { + if (currentScene.isPresent() && currentScene.get() == scene) { + continue; + } - boolean anyMatch = false; - for (ZombiesScene scene : router.getScenesContainingPlayer(sender.identity().uuid())) { - if (currentScene.isPresent() && currentScene.get() == scene) { - continue; + Stage stage = scene.getCurrentStage(); + + if (stage != null && stage.canRejoin()) { + targetGame = scene.getUUID(); + break; + } } - if (scene.getUUID().equals(targetGame)) { - anyMatch = true; - break; + if (targetGame == null) { + sender.sendMessage(Component.text("Couldn't find a game to rejoin!", NamedTextColor.RED)); + return; } } + else { + boolean anyMatch = false; + for (ZombiesScene scene : router.getScenesContainingPlayer(sender.identity().uuid())) { + if (currentScene.isPresent() && currentScene.get() == scene) { + continue; + } - if (!anyMatch) { - sender.sendMessage(Component.text("Invalid game!", NamedTextColor.RED)); - return; + if (scene.getUUID().equals(targetGame)) { + anyMatch = true; + break; + } + } + + if (!anyMatch) { + sender.sendMessage(Component.text("Invalid game!", NamedTextColor.RED)); + return; + } } joinHelper.rejoinGame(((Player)sender), targetGame); diff --git a/zombies/src/main/java/org/phantazm/zombies/corpse/CorpseCreator.java b/zombies/src/main/java/org/phantazm/zombies/corpse/CorpseCreator.java index 9c84de5ce..505d2038c 100644 --- a/zombies/src/main/java/org/phantazm/zombies/corpse/CorpseCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/corpse/CorpseCreator.java @@ -6,16 +6,20 @@ import com.github.steanky.ethylene.core.ConfigPrimitive; import com.github.steanky.ethylene.mapper.annotation.Default; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerSkin; import net.minestom.server.instance.Instance; import net.minestom.server.scoreboard.Team; +import org.apache.commons.lang3.RandomStringUtils; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Activable; -import org.phantazm.core.ComponentUtils; import org.phantazm.core.entity.fakeplayer.MinimalFakePlayer; import org.phantazm.core.hologram.Hologram; import org.phantazm.core.hologram.InstanceHologram; @@ -24,7 +28,6 @@ import org.phantazm.zombies.player.state.revive.ReviveHandler; import java.util.List; -import java.util.UUID; @Model("zombies.corpse") @Cache(false) @@ -33,6 +36,8 @@ public interface Source { @NotNull CorpseCreator make(@NotNull DependencyProvider mapDependencyProvider); } + private static final String POSSIBLE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"; + private final Data data; private final List idleLines; private final List revivingLines; @@ -50,9 +55,14 @@ public CorpseCreator(@NotNull Data data, @NotNull @Child("idle_lines") List { + for (EquipmentSlot slot : EquipmentSlot.values()) { + corpseEntity.setEquipment(slot, player.getEquipment(slot)); + } + }); Hologram hologram = new InstanceHologram(deathLocation.add(0, data.hologramHeightOffset, 0), data.hologramGap); @@ -114,6 +124,7 @@ public record Data(@NotNull Component message) { public static class TimeLine implements CorpseLine { private final Data data; private final TickFormatter tickFormatter; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); @FactoryMethod public TimeLine(@NotNull Data data, @NotNull @Child("tick_formatter") TickFormatter tickFormatter) { @@ -123,15 +134,15 @@ public TimeLine(@NotNull Data data, @NotNull @Child("tick_formatter") TickFormat @Override public @NotNull Component update(@NotNull Corpse corpse, long time) { - return ComponentUtils.tryFormat(data.formatString, tickFormatter.format(data.ticksUntilRevive - ? corpse.reviveHandler.getTicksUntilRevive() - : corpse.reviveHandler.getTicksUntilDeath())); + TagResolver ticksUntilRevivePlaceholder = Placeholder.unparsed("time_until_revive", + tickFormatter.format(corpse.reviveHandler.getTicksUntilRevive())); + TagResolver ticksUntilDeathPlaceholder = Placeholder.unparsed("time_until_death", + tickFormatter.format(corpse.reviveHandler.getTicksUntilDeath())); + return miniMessage.deserialize(data.format, ticksUntilRevivePlaceholder, ticksUntilDeathPlaceholder); } @DataObject - public record Data(@NotNull String formatString, - boolean ticksUntilRevive, - @NotNull @ChildPath("tick_formatter") String tickFormatter) { + public record Data(@NotNull String format, @NotNull @ChildPath("tick_formatter") String tickFormatter) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/damage/ZombiesDamageType.java b/zombies/src/main/java/org/phantazm/zombies/damage/ZombiesDamageType.java index 88cb2e941..9b1e9f98b 100644 --- a/zombies/src/main/java/org/phantazm/zombies/damage/ZombiesDamageType.java +++ b/zombies/src/main/java/org/phantazm/zombies/damage/ZombiesDamageType.java @@ -1,11 +1,7 @@ package org.phantazm.zombies.damage; -import net.minestom.server.entity.damage.DamageType; - public class ZombiesDamageType { - public static final DamageType BOMBING = new DamageType("zombies.bombing"); - private ZombiesDamageType() { throw new UnsupportedOperationException(); } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/Gun.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/Gun.java index 0447629dc..a648bf88f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/Gun.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/Gun.java @@ -31,7 +31,6 @@ public class Gun extends CachedInventoryObject implements Equipment, Upgradable private Key levelKey; private GunLevel level; private GunState state; - private boolean reloadComplete = false; /** * Creates a new gun. @@ -50,7 +49,7 @@ public Gun(@NotNull Key equipmentKey, @NotNull Supplier builder.setTicksSinceLastReload(0L)); - reloadComplete = false; + modifyState(builder -> { + builder.setTicksSinceLastReload(0L); + builder.setReloadComplete(false); + }); for (GunEffect reloadEffect : level.reloadEffects()) { reloadEffect.apply(state); } @@ -186,9 +187,9 @@ public void tick(long time) { if (level.reloadTester().isReloading(state)) { builder.setTicksSinceLastReload(builder.getTicksSinceLastReload() + 1); } - else if (!reloadComplete) { + else if (!builder.isReloadComplete()) { builder.setClip(Math.min(level.stats().maxClip(), getState().ammo())); - reloadComplete = true; + builder.setReloadComplete(true); } }); @@ -262,6 +263,8 @@ public void setLevel(@NotNull Key key) { for (GunEffect effect : level.activateEffects()) { effect.apply(state); } + + setDirty(); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunLevel.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunLevel.java index 30eb90bc9..7b9904c0f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunLevel.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunLevel.java @@ -32,6 +32,7 @@ * @param gunStackMappers The gun's {@link GunStackMapper}s that produce the visual {@link ItemStack} representation of the gun */ @Model("zombies.gun.level") +@Cache(false) public record GunLevel(@NotNull Data data, @NotNull @Child("stats") GunStats stats, @NotNull @Child("shoot_tester") ShootTester shootTester, diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunState.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunState.java index d80e57750..1bd4e3ab5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunState.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/GunState.java @@ -17,6 +17,7 @@ public record GunState(long ticksSinceLastShot, long ticksSinceLastFire, long ticksSinceLastReload, + boolean reloadComplete, int ammo, int clip, boolean isMainEquipment, @@ -29,8 +30,8 @@ public record GunState(long ticksSinceLastShot, */ public @NotNull GunState.Builder toBuilder() { return new Builder().setTicksSinceLastShot(ticksSinceLastShot).setTicksSinceLastFire(ticksSinceLastFire) - .setTicksSinceLastReload(ticksSinceLastReload).setAmmo(ammo).setClip(clip) - .setMainEquipment(isMainEquipment).setQueuedShots(queuedShots); + .setTicksSinceLastReload(ticksSinceLastReload).setReloadComplete(reloadComplete).setAmmo(ammo) + .setClip(clip).setMainEquipment(isMainEquipment).setQueuedShots(queuedShots); } /** @@ -44,6 +45,8 @@ public static class Builder { private long ticksSinceLastReload; + private boolean reloadComplete; + private int ammo; private int clip; @@ -112,6 +115,15 @@ public long getTicksSinceLastReload() { return this; } + public boolean isReloadComplete() { + return reloadComplete; + } + + public @NotNull Builder setReloadComplete(boolean reloadComplete) { + this.reloadComplete = reloadComplete; + return this; + } + /** * Gets the amount of ammo left in the gun. * @@ -198,8 +210,8 @@ public int getQueuedShots() { * @return A {@link GunState} representation of the {@link Builder} */ public @NotNull GunState build() { - return new GunState(ticksSinceLastShot, ticksSinceLastFire, ticksSinceLastReload, ammo, clip, - isMainEquipment, queuedShots); + return new GunState(ticksSinceLastShot, ticksSinceLastFire, ticksSinceLastReload, reloadComplete, ammo, + clip, isMainEquipment, queuedShots); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/ZombiesEquipmentModule.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/ZombiesEquipmentModule.java index c993244eb..edd9a7696 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/ZombiesEquipmentModule.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/ZombiesEquipmentModule.java @@ -12,9 +12,11 @@ import org.phantazm.mob.MobModel; import org.phantazm.mob.MobStore; import org.phantazm.mob.spawner.MobSpawner; +import org.phantazm.zombies.equipment.gun.action_bar.ZombiesPlayerActionBarSender; import org.phantazm.zombies.map.objects.MapObjects; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.stats.zombies.ZombiesPlayerMapStats; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import java.util.*; import java.util.function.Function; @@ -27,6 +29,7 @@ public class ZombiesEquipmentModule implements DependencyModule { private final Map playerMap; private final PlayerView playerView; private final ZombiesPlayerMapStats mapStats; + private final ZombiesPlayerActionBar actionBar; private final MobSpawner mobSpawner; private final MobStore mobStore; private final EventNode eventNode; @@ -36,13 +39,15 @@ public class ZombiesEquipmentModule implements DependencyModule { private final Function mobModelFunction; public ZombiesEquipmentModule(@NotNull Map playerMap, - @NotNull PlayerView playerView, @NotNull ZombiesPlayerMapStats mapStats, @NotNull MobSpawner mobSpawner, + @NotNull PlayerView playerView, @NotNull ZombiesPlayerMapStats mapStats, + @NotNull ZombiesPlayerActionBar actionBar, @NotNull MobSpawner mobSpawner, @NotNull MobStore mobStore, @NotNull EventNode eventNode, @NotNull Random random, @NotNull MapObjects mapObjects, @NotNull Supplier zombiesPlayerSupplier, @NotNull Function mobModelFunction) { this.playerMap = Objects.requireNonNull(playerMap, "playerMap"); this.playerView = Objects.requireNonNull(playerView, "playerView"); this.mapStats = Objects.requireNonNull(mapStats, "mapStats"); + this.actionBar = Objects.requireNonNull(actionBar, "actionBar"); this.mobSpawner = Objects.requireNonNull(mobSpawner, "mobSpawner"); this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); this.eventNode = Objects.requireNonNull(eventNode, "eventNode"); @@ -64,6 +69,10 @@ public ZombiesEquipmentModule(@NotNull Map> getPlayerSupplier() { return playerView::getPlayer; } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ActionBarSender.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ActionBarSender.java new file mode 100644 index 000000000..fb60fbb4d --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ActionBarSender.java @@ -0,0 +1,11 @@ +package org.phantazm.zombies.equipment.gun.action_bar; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface ActionBarSender { + + void sendActionBar(@NotNull Component message); + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ZombiesPlayerActionBarSender.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ZombiesPlayerActionBarSender.java new file mode 100644 index 000000000..52ed64e97 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/action_bar/ZombiesPlayerActionBarSender.java @@ -0,0 +1,37 @@ +package org.phantazm.zombies.equipment.gun.action_bar; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; + +import java.util.Objects; + +@Model("zombies.gun.action_bar.zombies_player_sender") +@Cache(false) +public class ZombiesPlayerActionBarSender implements ActionBarSender { + + private final Data data; + + private final ZombiesPlayerActionBar actionBar; + + @FactoryMethod + public ZombiesPlayerActionBarSender(@NotNull Data data, @NotNull ZombiesPlayerActionBar actionBar) { + this.data = Objects.requireNonNull(data, "data"); + this.actionBar = Objects.requireNonNull(actionBar, "actionBar"); + } + + @Override + public void sendActionBar(@NotNull Component message) { + actionBar.sendActionBar(message, data.priority()); + } + + @DataObject + public record Data(int priority) { + + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/AlertNoAmmoEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/AlertNoAmmoEffect.java new file mode 100644 index 000000000..f31370930 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/AlertNoAmmoEffect.java @@ -0,0 +1,67 @@ +package org.phantazm.zombies.equipment.gun.effect; + +import com.github.steanky.element.core.annotation.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.TitlePart; +import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.chat.MessageWithDestination; +import org.phantazm.zombies.equipment.gun.GunState; +import org.phantazm.zombies.equipment.gun.audience.AudienceProvider; + +import java.util.Objects; + +@Model("zombies.gun.effect.alert_no_ammo") +@Cache(false) +public class AlertNoAmmoEffect implements GunEffect { + + private final Data data; + + private final AudienceProvider audienceProvider; + + private boolean hadNoAmmo = false; + + @FactoryMethod + public AlertNoAmmoEffect(@NotNull Data data, + @NotNull @Child("audience_provider") AudienceProvider audienceProvider) { + this.data = Objects.requireNonNull(data, "data"); + this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + } + + @Override + public void apply(@NotNull GunState state) { + if (state.isMainEquipment()) { + if (state.ammo() == 0) { + displayMessage(data.message.component()); + hadNoAmmo = true; + } + } + else { + displayMessage(Component.empty()); + hadNoAmmo = false; + } + } + + private void displayMessage(Component component) { + audienceProvider.provideAudience().ifPresent(audience -> { + switch (data.message().destination()) { + case TITLE -> audience.sendTitlePart(TitlePart.TITLE, component); + case SUBTITLE -> audience.sendTitlePart(TitlePart.SUBTITLE, component); + case ACTION_BAR -> audience.sendActionBar(component); + } + }); + } + + @Override + public void tick(@NotNull GunState state, long time) { + if (state.isMainEquipment() && hadNoAmmo && state.ammo() > 0) { + displayMessage(Component.empty()); + hadNoAmmo = false; + } + } + + @DataObject + public record Data(@NotNull @ChildPath("audience_provider") String audienceProvider, + @NotNull MessageWithDestination message) { + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/PlaySoundEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/PlaySoundEffect.java index cfc6bbe41..32c691843 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/PlaySoundEffect.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/PlaySoundEffect.java @@ -27,7 +27,7 @@ public class PlaySoundEffect implements GunEffect { @FactoryMethod public PlaySoundEffect(@NotNull Data data, @NotNull @Child("audience_provider") AudienceProvider audienceProvider) { this.data = Objects.requireNonNull(data, "data"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + this.audienceProvider = Objects.requireNonNull(audienceProvider, "actionBarSender"); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/RecoilEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/RecoilEffect.java new file mode 100644 index 000000000..aa412d898 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/RecoilEffect.java @@ -0,0 +1,47 @@ +package org.phantazm.zombies.equipment.gun.effect; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.player.PlayerView; +import org.phantazm.zombies.equipment.gun.GunState; + +@Model("zombies.gun.effect.recoil") +@Cache(false) +public class RecoilEffect implements GunEffect { + private final Data data; + private final PlayerView playerView; + + @FactoryMethod + public RecoilEffect(@NotNull Data data, @NotNull PlayerView playerView) { + this.data = data; + this.playerView = playerView; + } + + @Override + public void apply(@NotNull GunState state) { + playerView.getPlayer().ifPresent(player -> { + Pos position = player.getPosition(); + + Vec lookDirection = position.direction(); + Vec knockbackDirection = lookDirection.mul(-1); + Pos direction = position.withDirection(knockbackDirection); + + double rad = Math.toRadians(direction.yaw()); + player.takeKnockback((float)data.knockbackStrength, true, Math.sin(rad), -Math.cos(rad)); + }); + } + + @Override + public void tick(@NotNull GunState state, long time) { + + } + + @DataObject + public record Data(double knockbackStrength) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/ReloadActionBarEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/ReloadActionBarEffect.java index 5f0c8abf2..3051e9ec0 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/ReloadActionBarEffect.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/ReloadActionBarEffect.java @@ -6,7 +6,7 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.GunStats; -import org.phantazm.zombies.equipment.gun.audience.AudienceProvider; +import org.phantazm.zombies.equipment.gun.action_bar.ActionBarSender; import org.phantazm.zombies.equipment.gun.reload.ReloadTester; import org.phantazm.zombies.equipment.gun.reload.actionbar.ReloadActionBarChooser; @@ -20,7 +20,7 @@ public class ReloadActionBarEffect implements GunEffect { private final GunStats stats; - private final AudienceProvider audienceProvider; + private final ActionBarSender actionBarSender; private final ReloadTester reloadTester; private final ReloadActionBarChooser chooser; private boolean active = false; @@ -29,17 +29,16 @@ public class ReloadActionBarEffect implements GunEffect { * Creates a {@link ReloadActionBarEffect}. * * @param stats The gun's {@link GunStats} - * @param audienceProvider A {@link AudienceProvider} to provide an {@link Audience} to send action bars to * @param reloadTester The gun's {@link ReloadTester} * @param chooser The {@link ReloadActionBarChooser} to choose an action bar to send to the {@link Audience} */ @FactoryMethod public ReloadActionBarEffect(@NotNull @Child("stats") GunStats stats, - @NotNull @Child("audience_provider") AudienceProvider audienceProvider, + @NotNull @Child("action_bar_sender") ActionBarSender actionBarSender, @NotNull @Child("reload_tester") ReloadTester reloadTester, @NotNull @Child("reload_action_bar_chooser") ReloadActionBarChooser chooser) { this.stats = Objects.requireNonNull(stats, "stats"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + this.actionBarSender = Objects.requireNonNull(actionBarSender, "actionBarSender"); this.reloadTester = Objects.requireNonNull(reloadTester, "reloadTester"); this.chooser = Objects.requireNonNull(chooser, "chooser"); } @@ -48,32 +47,29 @@ public ReloadActionBarEffect(@NotNull @Child("stats") GunStats stats, public void apply(@NotNull GunState state) { if (reloadTester.isReloading(state) && state.isMainEquipment()) { float progress = (float)state.ticksSinceLastReload() / stats.reloadSpeed(); - audienceProvider.provideAudience() - .ifPresent(audience -> audience.sendActionBar(chooser.choose(state, progress))); + actionBarSender.sendActionBar(chooser.choose(state, progress)); active = true; } - else if (active) { - audienceProvider.provideAudience().ifPresent(audience -> audience.sendActionBar(Component.empty())); - active = false; - } } @Override public void tick(@NotNull GunState state, long time) { - + if (!(reloadTester.isReloading(state) && state.isMainEquipment()) && active) { + actionBarSender.sendActionBar(Component.empty()); + active = false; + } } /** * Data for an {@link ReloadActionBarEffect}. * * @param stats A path to the guns's {@link GunStats} - * @param audienceProvider A path to the {@link ReloadActionBarEffect}'s {@link AudienceProvider} * @param reloadTester A path to the gun's {@link ReloadTester} * @param reloadActionBarChooser A path to the {@link ReloadActionBarEffect}'s {@link ReloadActionBarChooser} */ @DataObject public record Data(@NotNull @ChildPath("stats") String stats, - @NotNull @ChildPath("audience_provider") String audienceProvider, + @NotNull @ChildPath("action_bar_sender") String actionBarSender, @NotNull @ChildPath("reload_tester") String reloadTester, @NotNull @ChildPath("reload_action_bar_chooser") String reloadActionBarChooser) { } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/SendMessageEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/SendMessageEffect.java index 4f919041f..e18a3a55f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/SendMessageEffect.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/effect/SendMessageEffect.java @@ -3,12 +3,15 @@ import com.github.steanky.element.core.annotation.*; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.TitlePart; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.chat.MessageWithDestination; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.audience.AudienceProvider; import java.util.Objects; +// TODO: priority support, also do priorities for titles /** * A {@link GunEffect} that sends a message to an {@link Audience}. */ @@ -29,12 +32,19 @@ public class SendMessageEffect implements GunEffect { public SendMessageEffect(@NotNull Data data, @NotNull @Child("audience_provider") AudienceProvider audienceProvider) { this.data = Objects.requireNonNull(data, "data"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + this.audienceProvider = Objects.requireNonNull(audienceProvider, "actionBarSender"); } @Override public void apply(@NotNull GunState state) { - audienceProvider.provideAudience().ifPresent(audience -> audience.sendMessage(data.message())); + audienceProvider.provideAudience().ifPresent(audience -> { + switch (data.message().destination()) { + case TITLE -> audience.sendTitlePart(TitlePart.TITLE, data.message().component()); + case SUBTITLE -> audience.sendTitlePart(TitlePart.SUBTITLE, data.message().component()); + case CHAT -> audience.sendMessage(data.message().component()); + case ACTION_BAR -> audience.sendActionBar(data.message().component()); + } + }); } @Override @@ -49,6 +59,7 @@ public void tick(@NotNull GunState state, long time) { * @param message The {@link Component} to send to the {@link Audience} */ @DataObject - public record Data(@NotNull @ChildPath("audience_provider") String audienceProvider, @NotNull Component message) { + public record Data(@NotNull @ChildPath("audience_provider") String audienceProvider, + @NotNull MessageWithDestination message) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/reload/StateReloadTester.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/reload/StateReloadTester.java index 701ee2863..42e293c2d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/reload/StateReloadTester.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/reload/StateReloadTester.java @@ -33,7 +33,7 @@ public boolean shouldReload(@NotNull GunState state) { @Override public boolean canReload(@NotNull GunState state) { - return !isReloading(state) && state.ammo() > 0; + return !isReloading(state) && state.ammo() > 0 && state.reloadComplete(); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/StateShootTester.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/StateShootTester.java index edc2bf028..080b22ff3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/StateShootTester.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/StateShootTester.java @@ -2,9 +2,7 @@ import com.github.steanky.element.core.annotation.*; import net.minestom.server.entity.Entity; -import net.minestom.server.entity.LivingEntity; import org.jetbrains.annotations.NotNull; -import org.phantazm.zombies.Attributes; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.GunStats; import org.phantazm.zombies.equipment.gun.GunUtils; @@ -18,6 +16,7 @@ * A {@link ShootTester} based solely on {@link GunState}. */ @Model("zombies.gun.shoot_tester.state") +@Cache(false) public class StateShootTester implements ShootTester { private final GunStats stats; diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIteration.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIteration.java similarity index 64% rename from zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIteration.java rename to zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIteration.java index db9466f2b..9c9f6c3f8 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIteration.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIteration.java @@ -1,17 +1,14 @@ package org.phantazm.zombies.equipment.gun.shoot.blockiteration; -import com.github.steanky.element.core.annotation.Cache; -import com.github.steanky.element.core.annotation.DataObject; -import com.github.steanky.element.core.annotation.FactoryMethod; -import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.element.core.annotation.*; import net.kyori.adventure.key.Key; import net.minestom.server.collision.Shape; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.equipment.gun.shoot.wallshooting.WallshootingChecker; -import java.util.Collection; import java.util.Objects; import java.util.Set; @@ -24,14 +21,18 @@ * a {@link BasicBlockIteration}. */ @Model("zombies.gun.block_iteration.wallshot") -@Cache -public class WallshotBlockIteration implements BlockIteration { +@Cache(false) +public class WallshootingBlockIteration implements BlockIteration { private final Data data; + private final WallshootingChecker wallshootingChecker; + @FactoryMethod - public WallshotBlockIteration(@NotNull Data data) { + public WallshootingBlockIteration(@NotNull Data data, + @NotNull @Child("wallshooting_checker") WallshootingChecker wallshootingChecker) { this.data = Objects.requireNonNull(data, "data"); + this.wallshootingChecker = Objects.requireNonNull(wallshootingChecker, "wallshootingChecker"); } @Override @@ -42,15 +43,19 @@ public WallshotBlockIteration(@NotNull Data data) { @Override public boolean isValidEndpoint(@NotNull Point blockLocation, @NotNull Block block) { - return !wallshot; + if (wallshootingChecker.canWallshoot()) { + return !wallshot; + } + + return true; } @SuppressWarnings("UnstableApiUsage") @Override public boolean acceptRaytracedBlock(@NotNull Vec intersection, @NotNull Block block) { Shape blockShape = block.registry().collisionShape(); - if ((!blockShape.isFullBlock() && !blockShape.isEmpty()) || - data.passableBlocks().contains(block.key())) { + if (wallshootingChecker.canWallshoot() && ((!blockShape.isFullBlock() && !blockShape.isEmpty()) || + data.passableBlocks().contains(block.key()))) { wallshot = true; return false; } @@ -62,6 +67,7 @@ public boolean acceptRaytracedBlock(@NotNull Vec intersection, @NotNull Block bl } @DataObject - public record Data(@NotNull Set passableBlocks) { + public record Data(@NotNull @ChildPath("wallshooting_checker") String wallshootingChecker, + @NotNull Set passableBlocks) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/endpoint/BasicShotEndpointSelector.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/endpoint/BasicShotEndpointSelector.java index b437d2c4f..ad6a71ac2 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/endpoint/BasicShotEndpointSelector.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/endpoint/BasicShotEndpointSelector.java @@ -75,24 +75,27 @@ public BasicShotEndpointSelector(@NotNull Data data, @NotNull Supplier intersectionOptional = RayUtils.getIntersectionPosition(shape, blockLocation, start); - if (intersectionOptional.isPresent()) { - Vec intersection = intersectionOptional.get(); - for (BlockIteration.Context context : contexts) { - if (!context.acceptRaytracedBlock(intersection, block)) { - block = null; - continue blockLoop; - } - - return intersection; + if (intersectionOptional.isEmpty()) { + continue; + } + + Vec intersection = intersectionOptional.get(); + for (BlockIteration.Context context : contexts) { + if (!context.acceptRaytracedBlock(intersection, block)) { + block = null; + continue blockLoop; } } + + return intersection; } + Pos limit = start.add(start.direction().mul(data.maxDistance)); if (block != null) { - return RayUtils.rayTrace(block.registry().collisionShape(), blockLocation, start).orElse(null); + return RayUtils.rayTrace(block.registry().collisionShape(), blockLocation, start).orElse(limit.asVec()); } - return null; + return limit; }); } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/fire/SpreadFirer.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/fire/SpreadFirer.java index 35c6af764..ab822913d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/fire/SpreadFirer.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/fire/SpreadFirer.java @@ -1,8 +1,10 @@ package org.phantazm.zombies.equipment.gun.shoot.fire; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.equipment.gun.Gun; import org.phantazm.zombies.equipment.gun.GunState; @@ -19,7 +21,7 @@ public class SpreadFirer implements Firer { private final Data data; private final Random random; - private final Collection subFirers; + private final List subFirers; /** * Creates a {@link SpreadFirer}. @@ -39,25 +41,26 @@ public SpreadFirer(@NotNull Data data, @NotNull Random random, @Override public void fire(@NotNull Gun gun, @NotNull GunState state, @NotNull Pos start, @NotNull Collection previousHits) { + if (subFirers.isEmpty()) { + return; + } + if (data.angleVariance() == 0) { - for (Firer subFirer : subFirers) { - subFirer.fire(gun, state, start, previousHits); + for (int i = 0; i < Math.max(subFirers.size(), data.amount); i++) { + subFirers.get(i % subFirers.size()).fire(gun, state, start, previousHits); } + return; } - Vec direction = start.direction(); - double yaw = Math.atan2(direction.z(), direction.x()); - double noYMagnitude = Math.sqrt(direction.x() * direction.x() + direction.z() * direction.z()); - double pitch = Math.atan2(direction.y(), noYMagnitude); + double yaw = start.yaw(); + double pitch = start.pitch(); - for (Firer subFirer : subFirers) { + for (int i = 0; i < Math.max(subFirers.size(), data.amount); i++) { double newYaw = yaw + data.angleVariance() * (2 * random.nextDouble() - 1); double newPitch = pitch + data.angleVariance() * (2 * random.nextDouble() - 1); - - Vec newDirection = new Vec(Math.cos(newYaw) * Math.cos(newPitch), Math.sin(newPitch), - Math.sin(newYaw) * Math.cos(newPitch)); - subFirer.fire(gun, state, start.withDirection(newDirection), previousHits); + subFirers.get(i % subFirers.size()) + .fire(gun, state, start.withView((float)newYaw, (float)newPitch), previousHits); } } @@ -75,8 +78,13 @@ public void tick(@NotNull GunState state, long time) { * @param angleVariance The maximum angle variance for each sub-{@link Firer} */ @DataObject - public record Data(@NotNull @ChildPath("sub_firers") Collection subFirers, float angleVariance) { - + public record Data(@NotNull @ChildPath("sub_firers") Collection subFirers, + int amount, + float angleVariance) { + @Default("amount") + public static @NotNull ConfigElement defaultAmount() { + return ConfigPrimitive.of(-1); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/DamageShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/DamageShotHandler.java index 1ddb50ed6..240ba91fb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/DamageShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/DamageShotHandler.java @@ -6,12 +6,11 @@ import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; -import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.event.EventDispatcher; import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.MobStore; -import org.phantazm.mob.PhantazmMob; -import org.phantazm.zombies.Flags; +import org.phantazm.zombies.Attributes; +import org.phantazm.zombies.Tags; import org.phantazm.zombies.equipment.gun.Gun; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.shoot.GunHit; @@ -23,8 +22,7 @@ import java.util.UUID; /** - * A {@link ShotHandler} that deals damage to targets. Respects the instakill flag {@link Flags#INSTA_KILL}. Raises - * {@link EntityDamageByGunEvent}s on a successful hit. + * A {@link ShotHandler} that deals damage to targets. Raises {@link EntityDamageByGunEvent}s on a successful hit. */ @Model("zombies.gun.shot_handler.damage") @Cache(false) @@ -37,7 +35,7 @@ public class DamageShotHandler implements ShotHandler { * @param data The {@link Data} to use */ @FactoryMethod - public DamageShotHandler(@NotNull Data data, @NotNull MobStore mobStore) { + public DamageShotHandler(@NotNull Data data) { this.data = Objects.requireNonNull(data, "data"); } @@ -62,17 +60,25 @@ private void handleDamageTargets(Gun gun, Entity attacker, Collection ta } if (event.isInstakill()) { + targetEntity.setTag(Tags.LAST_HIT_BY, attacker.getUuid()); targetEntity.kill(); continue; } - DamageType damageType = DamageType.fromEntity(attacker); + if (attacker instanceof LivingEntity livingEntity) { + damage *= livingEntity.getAttributeValue(Attributes.DAMAGE_MULTIPLIER); + } + + if (headshot) { + damage *= target.entity().getAttributeValue(Attributes.HEADSHOT_DAMAGE_MULTIPLIER); + } + Damage damageType = Damage.fromEntity(attacker, damage); switch (data.armorBehavior) { - case ALWAYS_BYPASS -> targetEntity.damage(damageType, damage); - case NEVER_BYPASS -> targetEntity.damage(damageType, damage, false); - case BYPASS_ON_HEADSHOT -> targetEntity.damage(damageType, damage, headshot); - case BYPASS_ON_NON_HEADSHOT -> targetEntity.damage(damageType, damage, !headshot); + case ALWAYS_BYPASS -> targetEntity.damage(damageType, true); + case NEVER_BYPASS -> targetEntity.damage(damageType, false); + case BYPASS_ON_HEADSHOT -> targetEntity.damage(damageType, headshot); + case BYPASS_ON_NON_HEADSHOT -> targetEntity.damage(damageType, !headshot); } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/GiveCoinsShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/GiveCoinsShotHandler.java index 917571cb4..95890e9c2 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/GiveCoinsShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/GiveCoinsShotHandler.java @@ -4,6 +4,7 @@ import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; +import net.kyori.adventure.text.Component; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.Flags; @@ -17,10 +18,7 @@ import org.phantazm.zombies.map.objects.MapObjects; import org.phantazm.zombies.player.ZombiesPlayer; -import java.util.Collection; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; +import java.util.*; @Model("zombies.gun.shot_handler.give_coins") @Cache(false) @@ -54,18 +52,27 @@ public void handle(@NotNull Gun gun, @NotNull GunState state, @NotNull Entity at boolean isInstaKill = mapObjects.module().flags().hasFlag(Flags.INSTA_KILL); PlayerCoins coins = player.module().getCoins(); + Collection modifiers = + player.module().compositeTransactionModifiers().modifiers(ModifierSourceGroups.MOB_COIN_GAIN); - for (GunHit ignored : shot.regularTargets()) { - coins.runTransaction(new Transaction( - player.module().compositeTransactionModifiers().modifiers(ModifierSourceGroups.MOB_COIN_GAIN), - isInstaKill ? data.instaKillCoins : data.normalCoins)).applyIfAffordable(coins); + int change = 0; + + Collection displays = new ArrayList<>(2); + if (!shot.regularTargets().isEmpty()) { + displays.add(Component.text((isInstaKill ? "Insta Kill " : "") + shot.regularTargets().size() + "x")); + for (GunHit ignored : shot.regularTargets()) { + change += isInstaKill ? data.instaKillCoins : data.normalCoins; + } } - for (GunHit ignored : shot.headshotTargets()) { - coins.runTransaction(new Transaction( - player.module().compositeTransactionModifiers().modifiers(ModifierSourceGroups.MOB_COIN_GAIN), - isInstaKill ? data.instaKillCoins : data.headshotCoins)).applyIfAffordable(coins); + if (!shot.headshotTargets().isEmpty()) { + displays.add(Component.text((isInstaKill ? "Insta Kill " : "Critical Hit ") + shot.headshotTargets().size() + "x")); + for (GunHit ignored : shot.headshotTargets()) { + change += isInstaKill ? data.instaKillCoins : data.headshotCoins; + } } + + coins.runTransaction(new Transaction(modifiers, displays, change)).applyIfAffordable(coins); } @DataObject diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/IgniteShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/IgniteShotHandler.java index 87c85960d..c8a767b1c 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/IgniteShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/IgniteShotHandler.java @@ -10,6 +10,9 @@ import net.minestom.server.entity.damage.DamageType; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.ExtraNodeKeys; import org.phantazm.zombies.equipment.gun.Gun; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.shoot.GunHit; @@ -28,6 +31,7 @@ public class IgniteShotHandler implements ShotHandler { private final Tag lastFireDamage; private final Deque targets; + private final MobStore mobStore; /** * Creates an {@link IgniteShotHandler}. @@ -35,12 +39,13 @@ public class IgniteShotHandler implements ShotHandler { * @param data The {@link IgniteShotHandler}'s {@link Data} */ @FactoryMethod - public IgniteShotHandler(@NotNull Data data) { + public IgniteShotHandler(@NotNull Data data, @NotNull MobStore mobStore) { this.data = Objects.requireNonNull(data, "data"); UUID uuid = UUID.randomUUID(); this.lastFireDamage = Tag.Long("last_fire_damage_time" + uuid).defaultValue(-1L); this.targets = new ConcurrentLinkedDeque<>(); + this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); } @Override @@ -53,6 +58,11 @@ public void handle(@NotNull Gun gun, @NotNull GunState state, @NotNull Entity at private void setFire(Collection hits, int duration) { for (GunHit target : hits) { LivingEntity entity = target.entity(); + PhantazmMob mob = mobStore.getMob(entity.getUuid()); + if (mob != null && mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_FIRE)) { + continue; + } + entity.setFireForDuration(duration); boolean alreadyActive = entity.getTag(lastFireDamage) != -1; diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/MessageShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/MessageShotHandler.java index 8838eefaa..20838822e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/MessageShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/MessageShotHandler.java @@ -34,7 +34,7 @@ public class MessageShotHandler implements ShotHandler { public MessageShotHandler(@NotNull Data data, @NotNull @Child("audience_provider") AudienceProvider audienceProvider) { this.data = Objects.requireNonNull(data, "data"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + this.audienceProvider = Objects.requireNonNull(audienceProvider, "actionBarSender"); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SlowDownShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SlowDownShotHandler.java index d3c20a3ab..4c69c8b7b 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SlowDownShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SlowDownShotHandler.java @@ -14,6 +14,9 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.ExtraNodeKeys; import org.phantazm.zombies.equipment.gun.Gun; import org.phantazm.zombies.equipment.gun.GunState; import org.phantazm.zombies.equipment.gun.shoot.GunHit; @@ -33,11 +36,14 @@ public class SlowDownShotHandler implements ShotHandler { private final Data data; + private MobStore mobStore; + private long selfTick = 0; @FactoryMethod - public SlowDownShotHandler(@NotNull Data data) { + public SlowDownShotHandler(@NotNull Data data, @NotNull MobStore mobStore) { this.data = Objects.requireNonNull(data, "data"); + this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); } @Override @@ -69,6 +75,11 @@ public void tick(@NotNull GunState state, long time) { public void handle(@NotNull Gun gun, @NotNull GunState state, @NotNull Entity attacker, @NotNull Collection previousHits, @NotNull GunShot shot) { for (GunHit target : shot.regularTargets()) { + PhantazmMob mob = mobStore.getMob(target.entity().getUuid()); + if (mob != null && mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_SLOW_DOWN)) { + continue; + } + removalQueue.add(ObjectLongPair.of(target.entity().getUuid(), selfTick + data.headshotDuration())); latestTimeMap.put(target.entity().getUuid(), selfTick + data.duration()); AttributeModifier modifier = @@ -79,6 +90,11 @@ public void handle(@NotNull Gun gun, @NotNull GunState state, @NotNull Entity at attribute.addModifier(modifier); } for (GunHit target : shot.headshotTargets()) { + PhantazmMob mob = mobStore.getMob(target.entity().getUuid()); + if (mob != null && mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_SLOW_DOWN)) { + continue; + } + removalQueue.add(ObjectLongPair.of(target.entity().getUuid(), selfTick + data.headshotDuration())); latestTimeMap.put(target.entity().getUuid(), selfTick + data.headshotDuration()); AttributeModifier modifier = diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SoundShotHandler.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SoundShotHandler.java index 5551a2a45..f231056f0 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SoundShotHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/handler/SoundShotHandler.java @@ -32,7 +32,7 @@ public class SoundShotHandler implements ShotHandler { public SoundShotHandler(@NotNull Data data, @NotNull @Child("audience_provider") AudienceProvider audienceProvider) { this.data = Objects.requireNonNull(data, "data"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider"); + this.audienceProvider = Objects.requireNonNull(audienceProvider, "actionBarSender"); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/MapObjectsWallshootingChecker.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/MapObjectsWallshootingChecker.java new file mode 100644 index 000000000..3266c70f6 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/MapObjectsWallshootingChecker.java @@ -0,0 +1,27 @@ +package org.phantazm.zombies.equipment.gun.shoot.wallshooting; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.Flags; +import org.phantazm.zombies.map.objects.MapObjects; + +import java.util.Objects; + +@Model("zombies.gun.wallshooting.wallshooting_checker.map_objects") +@Cache(false) +public class MapObjectsWallshootingChecker implements WallshootingChecker { + + private final MapObjects mapObjects; + + @FactoryMethod + public MapObjectsWallshootingChecker(@NotNull MapObjects mapObjects) { + this.mapObjects = Objects.requireNonNull(mapObjects, "mapObjects"); + } + + @Override + public boolean canWallshoot() { + return mapObjects.module().flags().hasFlag(Flags.WALLSHOOTING_ENABLED); + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/WallshootingChecker.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/WallshootingChecker.java new file mode 100644 index 000000000..c9c1674cd --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/shoot/wallshooting/WallshootingChecker.java @@ -0,0 +1,7 @@ +package org.phantazm.zombies.equipment.gun.shoot.wallshooting; + +public interface WallshootingChecker { + + boolean canWallshoot(); + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/target/tester/PhantazmMobTargetTester.java b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/target/tester/PhantazmMobTargetTester.java index a3a642fde..888a21cf3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/gun/target/tester/PhantazmMobTargetTester.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/gun/target/tester/PhantazmMobTargetTester.java @@ -5,33 +5,24 @@ import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; -import org.phantazm.mob.MobStore; -import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.map.objects.MapObjects; import java.util.Collection; -import java.util.Objects; import java.util.UUID; -/** - * A {@link TargetTester} that only selects {@link PhantazmMob}s. - */ @Model("zombies.gun.target_tester.phantazm_mob") @Cache(false) public class PhantazmMobTargetTester implements TargetTester { - private final MobStore mobStore; + private final MapObjects mapObjects; - /** - * Creates a {@link PhantazmMobTargetTester}. - * - * @param mobStore The {@link MobStore} to retrieve {@link PhantazmMob}s from - */ @FactoryMethod - public PhantazmMobTargetTester(@NotNull MobStore mobStore) { - this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); + public PhantazmMobTargetTester(@NotNull MapObjects mapObjects) { + this.mapObjects = mapObjects; } @Override public boolean useTarget(@NotNull Entity target, @NotNull Collection previousHits) { - return mobStore.getMob(target.getUuid()) != null; + return mapObjects.module().roundHandlerSupplier().get().currentRound() + .map(round -> round.hasMob(target.getUuid())).orElse(false); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/AddGroupSlotsCreator.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/AddGroupSlotsCreator.java index 83299e7c4..0050012e1 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/AddGroupSlotsCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/AddGroupSlotsCreator.java @@ -9,6 +9,7 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.core.equipment.EquipmentHandler; +import org.phantazm.core.inventory.InventoryAccessRegistry; import org.phantazm.core.inventory.InventoryObjectGroup; import org.phantazm.zombies.player.ZombiesPlayer; @@ -64,7 +65,9 @@ public void start() { @Override public void end() { - handler.accessRegistry().getCurrentAccess().ifPresent(access -> { + InventoryAccessRegistry accessRegistry = handler.accessRegistry(); + + accessRegistry.getCurrentAccess().ifPresent(access -> { InventoryObjectGroup group = access.groups().get(data.group); if (group == null) { return; @@ -74,6 +77,7 @@ public void end() { for (int slot : data.additionalSlots) { if (existingSlots.contains(slot)) { group.removeSlot(slot); + accessRegistry.removeObject(access, slot); } } }); diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/TransactionModifierPerkEffectCreator.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/TransactionModifierPerkEffectCreator.java new file mode 100644 index 000000000..b2a8c8599 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/TransactionModifierPerkEffectCreator.java @@ -0,0 +1,87 @@ +package org.phantazm.zombies.equipment.perk.effect; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.coin.Transaction; +import org.phantazm.zombies.player.ZombiesPlayer; + +@Model("zombies.perk.effect.transaction_modifier") +@Cache(false) +public class TransactionModifierPerkEffectCreator implements PerkEffectCreator { + private final Data data; + + @FactoryMethod + public TransactionModifierPerkEffectCreator(@NotNull Data data) { + this.data = data; + } + + @Override + public @NotNull PerkEffect forPlayer(@NotNull ZombiesPlayer zombiesPlayer) { + return new Effect(data, zombiesPlayer); + } + + private static class Effect implements PerkEffect { + private final Data data; + private final ZombiesPlayer zombiesPlayer; + private final Transaction.Modifier modifier; + + private Effect(Data data, ZombiesPlayer zombiesPlayer) { + this.data = data; + this.zombiesPlayer = zombiesPlayer; + this.modifier = new Transaction.Modifier() { + @Override + public @NotNull Component getDisplayName() { + return data.modifierName; + } + + @Override + public int modify(int coins) { + return switch (data.modifierAction) { + case ADD -> (int)Math.rint(coins + data.amount); + case MULTIPLY -> (int)Math.rint(coins + (data.amount * coins)); + }; + } + + @Override + public int getPriority() { + return data.priority; + } + }; + } + + @Override + public void start() { + zombiesPlayer.module().playerTransactionModifiers().addModifier(data.group, modifier); + } + + @Override + public void end() { + zombiesPlayer.module().playerTransactionModifiers().removeModifier(data.group, modifier); + } + } + + public enum ModifierAction { + ADD, + MULTIPLY + } + + @DataObject + public record Data(@NotNull Key group, + @NotNull Component modifierName, + @NotNull ModifierAction modifierAction, + int priority, + double amount) { + @Default("priority") + public static @NotNull ConfigElement priorityDefault() { + return ConfigPrimitive.of(0); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyAttributeShotEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyAttributeShotEffect.java index 255d21651..c6557fbdc 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyAttributeShotEffect.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyAttributeShotEffect.java @@ -14,7 +14,10 @@ import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Tickable; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.Attributes; +import org.phantazm.zombies.ExtraNodeKeys; import org.phantazm.zombies.player.ZombiesPlayer; import java.util.Deque; @@ -42,8 +45,10 @@ public class ApplyAttributeShotEffect implements ShotEffect, Tickable { private final Deque entities; private final Tag applyTime; + private final MobStore mobStore; + @FactoryMethod - public ApplyAttributeShotEffect(@NotNull Data data) { + public ApplyAttributeShotEffect(@NotNull Data data, @NotNull MobStore mobStore) { this.data = Objects.requireNonNull(data, "data"); this.attributeUUID = UUID.randomUUID(); this.attributeName = this.attributeUUID.toString(); @@ -53,6 +58,7 @@ public ApplyAttributeShotEffect(@NotNull Data data) { String name = NAMES.computeIfAbsent(data, ignored -> UUID.randomUUID().toString()); this.entities = new ConcurrentLinkedDeque<>(); this.applyTime = Tag.Long(name).defaultValue(-1L); + this.mobStore = mobStore; } @Override @@ -61,6 +67,12 @@ public void perform(@NotNull Entity entity, @NotNull ZombiesPlayer zombiesPlayer return; } + PhantazmMob mob = mobStore.getMob(entity.getUuid()); + if (mob != null && data.amount < 0 && attribute.equals(Attribute.MOVEMENT_SPEED) && + mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_SLOW_DOWN)) { + return; + } + long tag = livingEntity.getTag(applyTime); livingEntity.setTag(applyTime, System.currentTimeMillis()); diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyFireShotEffect.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyFireShotEffect.java index a3187f85e..daebaf40a 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyFireShotEffect.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/effect/shot/ApplyFireShotEffect.java @@ -8,10 +8,14 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Tickable; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.ExtraNodeKeys; import org.phantazm.zombies.Tags; import org.phantazm.zombies.player.ZombiesPlayer; @@ -29,18 +33,20 @@ public class ApplyFireShotEffect implements ShotEffect, Tickable { private final Data data; private final Tag lastDamageTime; private final Deque activeEntities; + private final MobStore mobStore; private record DamageTarget(UUID damager, LivingEntity target) { } @FactoryMethod - public ApplyFireShotEffect(@NotNull Data data) { + public ApplyFireShotEffect(@NotNull Data data, @NotNull MobStore mobStore) { this.data = Objects.requireNonNull(data, "data"); UUID uuid = UUID.randomUUID(); this.lastDamageTime = Tag.Long("last_fire_damage_time_" + uuid).defaultValue(-1L); this.activeEntities = new ConcurrentLinkedDeque<>(); + this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); } @Override @@ -50,6 +56,11 @@ public void perform(@NotNull Entity entity, @NotNull ZombiesPlayer zombiesPlayer return; } + PhantazmMob mob = mobStore.getMob(entity.getUuid()); + if (mob != null && mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_FIRE)) { + return; + } + livingEntity.setFireForDuration(data.fireTicks); boolean alreadyActive = entity.getTag(lastDamageTime) != -1; @@ -81,10 +92,10 @@ public void tick(long time) { } private void doDamage(LivingEntity entity, UUID damager) { - DamageType damageType = new DamageType(DamageType.ON_FIRE.getIdentifier()); - damageType.setTag(Tags.LAST_HIT_BY, damager); + Damage damage = new Damage(DamageType.ON_FIRE, null, null, null, data.damage); + damage.setTag(Tags.LAST_HIT_BY, damager); - entity.damage(damageType, data.damage, data.bypassArmor); + entity.damage(damage, data.bypassArmor); } private void stopFire(Entity entity) { diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/equipment/interactor/MeleeInteractorCreator.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/equipment/interactor/MeleeInteractorCreator.java index b08d5de32..fd1ce2082 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/equipment/interactor/MeleeInteractorCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/equipment/interactor/MeleeInteractorCreator.java @@ -12,7 +12,7 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.LivingEntity; -import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; @@ -21,6 +21,7 @@ import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.ExtraNodeKeys; import org.phantazm.zombies.Flags; +import org.phantazm.zombies.Tags; import org.phantazm.zombies.coin.ModifierSourceGroups; import org.phantazm.zombies.coin.PlayerCoins; import org.phantazm.zombies.coin.Transaction; @@ -82,7 +83,7 @@ public boolean leftClick() { Pos feetPos = player.getPosition(); Pos eyePos = feetPos.add(0, player.getEyeHeight(), 0); - Point targetPos = eyePos.add(feetPos.direction().mul(data.reach)); + Point targetPos = eyePos.add(feetPos.direction().mul(data.reach + 5)); Wrapper closest = Wrapper.ofNull(); instance.getEntityTracker() @@ -112,11 +113,12 @@ public boolean leftClick() { if ((mapFlags.hasFlag(Flags.INSTA_KILL) || zombiesPlayer.flags().hasFlag(Flags.INSTA_KILL)) && (hitMob != null && !hitMob.model().getExtraNode() .getBooleanOrDefault(false, ExtraNodeKeys.RESIST_INSTAKILL))) { + hit.entity.setTag(Tags.LAST_HIT_BY, player.getUuid()); hit.entity.kill(); } else { double angle = feetPos.yaw() * (Math.PI / 180); - hit.entity.damage(DamageType.fromPlayer(player), data.damage, data.bypassArmor); + hit.entity.damage(Damage.fromPlayer(player, data.damage), data.bypassArmor); hit.entity.takeKnockback(data.knockback, Math.sin(angle), -Math.cos(angle)); } diff --git a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/level/UpgradeablePerkLevelCreator.java b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/level/UpgradeablePerkLevelCreator.java index a804c5ddd..1525f4201 100644 --- a/zombies/src/main/java/org/phantazm/zombies/equipment/perk/level/UpgradeablePerkLevelCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/equipment/perk/level/UpgradeablePerkLevelCreator.java @@ -57,6 +57,6 @@ public record Data(@NotNull @Description("The level key for this level") Key key @NotNull @Description("The equipment controlling this perk's visuals") @ChildPath( "equipment") String equipment, @NotNull @Description("The perk effect(s) which are applied for this level") @ChildPath( - "perk_effects") String perkEffects) { + "perk_effects") List effects) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/event/PhantazmMobDeathEvent.java b/zombies/src/main/java/org/phantazm/zombies/event/PhantazmMobDeathEvent.java new file mode 100644 index 000000000..eb80287b7 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/event/PhantazmMobDeathEvent.java @@ -0,0 +1,18 @@ +package org.phantazm.zombies.event; + +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.PhantazmMobInstanceEvent; + +public class PhantazmMobDeathEvent implements PhantazmMobInstanceEvent { + private final PhantazmMob phantazmMob; + + public PhantazmMobDeathEvent(@NotNull PhantazmMob phantazmMob) { + this.phantazmMob = phantazmMob; + } + + @Override + public @NotNull PhantazmMob getPhantazmMob() { + return phantazmMob; + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/event/ZombiesPlayerDeathEvent.java b/zombies/src/main/java/org/phantazm/zombies/event/ZombiesPlayerDeathEvent.java index 52f5031b8..fb1d4de60 100644 --- a/zombies/src/main/java/org/phantazm/zombies/event/ZombiesPlayerDeathEvent.java +++ b/zombies/src/main/java/org/phantazm/zombies/event/ZombiesPlayerDeathEvent.java @@ -1,6 +1,7 @@ package org.phantazm.zombies.event; 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.trait.CancellableEvent; import org.jetbrains.annotations.NotNull; @@ -11,15 +12,15 @@ public class ZombiesPlayerDeathEvent implements ZombiesPlayerEvent, CancellableEvent { private final Player player; private final ZombiesPlayer zombiesPlayer; - private final DamageType damageType; + private final Damage damage; private boolean cancelled; public ZombiesPlayerDeathEvent(@NotNull Player player, @NotNull ZombiesPlayer zombiesPlayer, - @NotNull DamageType damageType) { + @NotNull Damage damage) { this.player = Objects.requireNonNull(player, "player"); this.zombiesPlayer = Objects.requireNonNull(zombiesPlayer, "zombiesPlayer"); - this.damageType = Objects.requireNonNull(damageType, "damageType"); + this.damage = Objects.requireNonNull(damage, "damageType"); } @Override @@ -32,8 +33,8 @@ public ZombiesPlayerDeathEvent(@NotNull Player player, @NotNull ZombiesPlayer zo return zombiesPlayer; } - public @NotNull DamageType damageType() { - return damageType; + public @NotNull Damage damageType() { + return damage; } public void setCancelled(boolean cancel) { diff --git a/zombies/src/main/java/org/phantazm/zombies/leaderboard/BestTimeLeaderboard.java b/zombies/src/main/java/org/phantazm/zombies/leaderboard/BestTimeLeaderboard.java index 835c3b56a..d36de8b8d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/leaderboard/BestTimeLeaderboard.java +++ b/zombies/src/main/java/org/phantazm/zombies/leaderboard/BestTimeLeaderboard.java @@ -1,108 +1,295 @@ package org.phantazm.zombies.leaderboard; -import net.kyori.adventure.key.Key; +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.element.core.dependency.DependencyModule; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.core.hologram.Hologram; import org.phantazm.core.player.PlayerViewProvider; import org.phantazm.core.time.TickFormatter; import org.phantazm.stats.zombies.BestTime; import org.phantazm.stats.zombies.ZombiesDatabase; +import org.phantazm.zombies.map.MapSettingsInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; +import java.util.*; +@Model("zombies.leaderboard.best_time") +@Cache(false) public class BestTimeLeaderboard { private static final Logger LOGGER = LoggerFactory.getLogger(BestTimeLeaderboard.class); - private final ZombiesDatabase database; + private final Data data; - private final Hologram hologram; + private final ZombiesDatabase database; - private final Component mapName; + private final UUID viewer; - private final Key mapKey; + private final Hologram hologram; - private final TickFormatter tickFormatter; + private final MapSettingsInfo settings; private final PlayerViewProvider viewProvider; - private final int maxLength; + private final MiniMessage miniMessage; - private final Collection> nameFutures = new ArrayList<>(); + private final TickFormatter tickFormatter; + + private final Object sync = new Object(); - private Future bestTimesFuture = null; + private volatile boolean active = false; - public BestTimeLeaderboard(@NotNull ZombiesDatabase database, @NotNull Hologram hologram, - @NotNull Component mapName, @NotNull Key mapKey, @NotNull TickFormatter tickFormatter, - @NotNull PlayerViewProvider viewProvider, int maxLength) { + @FactoryMethod + public BestTimeLeaderboard(@NotNull Data data, @NotNull ZombiesDatabase database, @NotNull UUID viewer, + @NotNull Hologram hologram, @NotNull MapSettingsInfo settings, @NotNull PlayerViewProvider viewProvider, + @NotNull MiniMessage miniMessage, @NotNull @Child("tick_formatter") TickFormatter tickFormatter) { + this.data = Objects.requireNonNull(data, "data"); this.database = Objects.requireNonNull(database, "database"); + this.viewer = Objects.requireNonNull(viewer, "viewer"); this.hologram = Objects.requireNonNull(hologram, "hologram"); - this.mapName = Objects.requireNonNull(mapName, "mapName"); - this.mapKey = Objects.requireNonNull(mapKey, "mapKey"); - this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); + this.settings = Objects.requireNonNull(settings, "settings"); this.viewProvider = Objects.requireNonNull(viewProvider, "viewProvider"); - this.maxLength = maxLength; + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); + } + + public void startIfNotActive() { + synchronized (sync) { + if (active) { + return; + } + + TagResolver mapNamePlaceholder = Placeholder.component("map_name", settings.displayName()); + TagResolver viewerPlaceholder = Placeholder.component("viewer", data.initialMessage()); + for (String headerFormat : data.headerFormats()) { + hologram.add(miniMessage.deserialize(headerFormat, mapNamePlaceholder, viewerPlaceholder)); + } + for (int i = 0; i < data.length(); ++i) { + TagResolver placePlaceholder = Placeholder.component("place", Component.text(i + 1)); + Component placeMessage = miniMessage.deserialize(data.placeFormat(), placePlaceholder); + hologram.add(placeMessage); + } + for (String footerFormat : data.footerFormats()) { + hologram.add(miniMessage.deserialize(footerFormat, mapNamePlaceholder, viewerPlaceholder)); + } + + updateBody(); + updateViewerTime(mapNamePlaceholder); + active = true; + } } - public void start() { - hologram.add(Component.textOfChildren(Component.text("Zombies - "), mapName)); - for (int i = 0; i < maxLength; ++i) { - hologram.add(Component.text((i + 1) + ". - Loading...")); + public void endIfActive() { + synchronized (sync) { + if (active) { + hologram.clear(); + active = false; + } } + } - bestTimesFuture = database.getBestTimes(mapKey).whenComplete(((bestTimes, throwable) -> { + private void updateBody() { + database.getBestTimes(settings.id(), data.length()).whenComplete((bestTimes, throwable) -> { if (throwable != null) { - LOGGER.warn("Failed to fetch best times", throwable); + LOGGER.warn("Failed to fetch best times for {}", settings.id()); return; } - updateBestTimes(bestTimes); - })); + synchronized (sync) { + if (!active) { + return; + } + + trimBody(bestTimes); + + for (int i = 0; i < bestTimes.size(); ++i) { + BestTime bestTime = bestTimes.get(i); + int index = data.headerFormats().size() + i; + hologram.set(index, makePlaceTimeMessage(i + 1, bestTime)); + updateBodyName(index, i + 1, bestTime); + } + } + }); } - public void updateBestTimes(@NotNull List bestTimes) { - while (hologram.size() != bestTimes.size() + 1) { - hologram.remove(hologram.size() - 1); + private void trimBody(List bestTimes) { + for (int i = data.length - 1; i >= bestTimes.size(); --i) { + hologram.remove(data.headerFormats().size() + i); } + } + + private void updateBodyName(int index, int place, BestTime bestTime) { + viewProvider.fromUUID(bestTime.uuid()).getDisplayName().whenComplete((displayName, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to get display name for {}", bestTime.uuid(), throwable); + return; + } + + synchronized (sync) { + if (!active) { + return; + } + } + + hologram.set(index, makePlaceNameTimeMessage(place, displayName, bestTime)); + }); + } + + private void updateViewerTime(TagResolver mapNamePlaceholder) { + database.getBestTime(viewer, settings.id()).whenComplete((bestTimeOptional, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fetch best time for {} on {}", viewer, settings.id(), throwable); + return; + } + + synchronized (sync) { + if (!active) { + return; + } - for (int i = 0; i < bestTimes.size(); ++i) { - BestTime time = bestTimes.get(i); - Component timeComponent = Component.text(tickFormatter.format(time.time())); - hologram.set(i, Component.textOfChildren(Component.text((i + 1) + ". - Loading... - "), timeComponent)); - - int finalI = i; - CompletableFuture future = - viewProvider.fromUUID(time.uuid()).getDisplayName().whenComplete((displayName, throwable) -> { - if (throwable != null) { - LOGGER.warn("Failed to fetch display name for {}", time.uuid()); - return; - } - - hologram.set(finalI, - Component.textOfChildren(Component.text((finalI + 1) + ". - "), displayName, - Component.text(" - "), timeComponent)); - }); - nameFutures.add(future); + Component updatedViewerComponent; + if (bestTimeOptional.isPresent()) { + BestTime bestTime = bestTimeOptional.get(); + updatedViewerComponent = makePlaceTimeMessage(bestTime.rank(), bestTime); + } + else { + updatedViewerComponent = miniMessage.deserialize(data.noneFormat()); + } + + updateHeaderFooter(mapNamePlaceholder, Placeholder.component("viewer", updatedViewerComponent)); + updateViewerName(mapNamePlaceholder, bestTimeOptional.orElse(null)); + } + }); + } + + private void updateViewerName(TagResolver mapNamePlaceholder, BestTime bestTime) { + viewProvider.fromUUID(viewer).getDisplayName().whenComplete((displayName, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fetch name for {}", viewer, throwable); + return; + } + + synchronized (sync) { + if (!active) { + return; + } + + Component updatedViewerComponent; + if (bestTime != null) { + updatedViewerComponent = makePlaceNameTimeMessage(bestTime.rank(), displayName, bestTime); + } + else { + updatedViewerComponent = miniMessage.deserialize(data.noneNameFormat(), + Placeholder.component("player_name", displayName)); + } + + updateHeaderFooter(mapNamePlaceholder, Placeholder.component("viewer", updatedViewerComponent)); + } + }); + } + + private void updateHeaderFooter(TagResolver mapNamePlaceholder, TagResolver viewerPlaceholder) { + for (int i = 0; i < data.headerFormats().size(); ++i) { + String headerFormat = data.headerFormats().get(i); + hologram.set(i, miniMessage.deserialize(headerFormat, mapNamePlaceholder, viewerPlaceholder)); + } + + for (int i = 0; i < data.footerFormats().size(); ++i) { + String footerFormat = data.footerFormats().get(data.footerFormats().size() - 1 - i); + hologram.set(hologram.size() - 1 - i, + miniMessage.deserialize(footerFormat, mapNamePlaceholder, viewerPlaceholder)); } } - public void end() { - if (bestTimesFuture != null) { - bestTimesFuture.cancel(true); + private Component makePlaceTimeMessage(int place, BestTime bestTime) { + TagResolver placePlaceholder = Placeholder.component("place", Component.text(place)); + TagResolver timePlaceholder = + Placeholder.component("time", Component.text(tickFormatter.format(bestTime.time()))); + TagResolver isViewerPlaceholder = Formatter.choice("is_viewer", bestTime.uuid().equals(viewer) ? 1 : 0); + + return miniMessage.deserialize(data.placeTimeFormat(), placePlaceholder, timePlaceholder, isViewerPlaceholder); + } + + private Component makePlaceNameTimeMessage(int place, Component playerName, BestTime bestTime) { + TagResolver placePlaceholder = Placeholder.component("place", Component.text(place)); + TagResolver playerNamePlaceholder = Placeholder.component("player_name", playerName); + TagResolver timePlaceholder = + Placeholder.component("time", Component.text(tickFormatter.format(bestTime.time()))); + TagResolver isViewerPlaceholder = Formatter.choice("is_viewer", bestTime.uuid().equals(viewer) ? 1 : 0); + + return miniMessage.deserialize(data.placeNameTimeFormat(), placePlaceholder, playerNamePlaceholder, + timePlaceholder, isViewerPlaceholder); + } + + @Depend + public static class Module implements DependencyModule { + + private final ZombiesDatabase database; + + private final Hologram hologram; + + private final UUID viewer; + + private final MapSettingsInfo settings; + + private final PlayerViewProvider viewProvider; + + private final MiniMessage miniMessage; + + public Module(@NotNull ZombiesDatabase database, @NotNull UUID viewer, @NotNull Hologram hologram, + @NotNull MapSettingsInfo settings, @NotNull PlayerViewProvider viewProvider, + @NotNull MiniMessage miniMessage) { + this.database = Objects.requireNonNull(database, "database"); + this.viewer = Objects.requireNonNull(viewer, "viewer"); + this.hologram = Objects.requireNonNull(hologram, "hologram"); + this.settings = Objects.requireNonNull(settings, "settings"); + this.viewProvider = Objects.requireNonNull(viewProvider, "viewProvider"); + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + } + + public @NotNull ZombiesDatabase getDatabase() { + return database; + } + + public @NotNull UUID getViewer() { + return viewer; } - for (Future future : nameFutures) { - future.cancel(true); + public @NotNull Hologram getHologram() { + return hologram; } - nameFutures.clear(); + + public @NotNull MapSettingsInfo getSettings() { + return settings; + } + + public @NotNull PlayerViewProvider getViewProvider() { + return viewProvider; + } + + public @NotNull MiniMessage getMiniMessage() { + return miniMessage; + } + } + + @DataObject + public record Data(@NotNull @ChildPath("tick_formatter") String tickFormatter, + @NotNull List headerFormats, + @NotNull Component initialMessage, + @NotNull String noneFormat, + @NotNull String noneNameFormat, + @NotNull String placeFormat, + @NotNull String placeTimeFormat, + @NotNull String placeNameTimeFormat, + @NotNull List footerFormats, + int length) { + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PhantazmMobDeathListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PhantazmMobDeathListener.java index 61a89511a..5a13aac20 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PhantazmMobDeathListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PhantazmMobDeathListener.java @@ -11,6 +11,7 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.entity.EntityDeathEvent; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; @@ -20,6 +21,7 @@ import org.phantazm.mob.MobStore; import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.Tags; +import org.phantazm.zombies.event.PhantazmMobDeathEvent; import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.Round; import org.phantazm.zombies.map.Window; @@ -65,6 +67,8 @@ public PhantazmMobDeathListener(@NotNull KeyParser keyParser, @NotNull Instance @Override public void accept(@NotNull PhantazmMob mob, @NotNull EntityDeathEvent event) { + EventDispatcher.call(new PhantazmMobDeathEvent(mob)); + roundSupplier.get().ifPresent(round -> { round.removeMob(mob); }); diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageEventListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageEventListener.java index e28f101db..b9ac5570e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageEventListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageEventListener.java @@ -1,20 +1,21 @@ package org.phantazm.zombies.listener; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.collision.PhysicsResult; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; -import net.minestom.server.entity.damage.DamageType; -import net.minestom.server.entity.damage.EntityDamage; -import net.minestom.server.entity.damage.EntityProjectileDamage; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.entity.EntityDamageEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.PhysicsUtils; import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.Flags; -import org.phantazm.zombies.damage.ZombiesDamageType; +import org.phantazm.zombies.Tags; import org.phantazm.zombies.event.ZombiesPlayerDeathEvent; import org.phantazm.zombies.map.objects.MapObjects; import org.phantazm.zombies.player.ZombiesPlayer; @@ -48,7 +49,7 @@ protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull EntityDamag return; } - if (event.getDamage() < event.getEntity().getHealth()) { + if (event.getDamage().getAmount() < event.getEntity().getHealth()) { return; } @@ -58,7 +59,7 @@ protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull EntityDamag if (playerOptional.isPresent()) { Player player = playerOptional.get(); ZombiesPlayerDeathEvent deathEvent = - new ZombiesPlayerDeathEvent(player, zombiesPlayer, event.getDamageType()); + new ZombiesPlayerDeathEvent(player, zombiesPlayer, event.getDamage()); EventDispatcher.call(deathEvent); if (deathEvent.isCancelled()) { @@ -69,6 +70,14 @@ protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull EntityDamag } Pos deathPosition = event.getEntity().getPosition(); + // +2 just to sidestep any block border issues + double heightAboveBottom = deathPosition.y() - event.getInstance().getDimensionType().getMinY() + 2; + PhysicsResult collision = CollisionUtils.handlePhysics(event.getEntity(), new Vec(0, -heightAboveBottom, 0)); + if (PhysicsUtils.hasCollision(collision)) { + deathPosition = collision.newPosition(); + event.getEntity().teleport(deathPosition).join(); + } + Component killer = getKiller(event); Component roomName = getRoomName(deathPosition); @@ -92,16 +101,13 @@ private Component getEntityName(@NotNull Entity entity) { } private Component getKiller(@NotNull EntityDamageEvent event) { - DamageType damageType = event.getDamageType(); - - if (damageType instanceof EntityDamage entityDamage) { - return getEntityName(entityDamage.getSource()); - } - else if (damageType instanceof EntityProjectileDamage projectileDamage) { - Entity shooter = projectileDamage.getShooter(); - return getEntityName(Objects.requireNonNullElseGet(shooter, projectileDamage::getProjectile)); - } else if (damageType.getIdentifier().equals(ZombiesDamageType.BOMBING.getIdentifier())) { - return Component.text("Bombing", NamedTextColor.DARK_RED); + Damage damage = event.getDamage(); + if (damage.getAttacker() != null) { + return getEntityName(damage.getAttacker()); + } else if (damage.getSource() != null) { + return getEntityName(damage.getSource()); + } else if (damage.hasTag(Tags.DAMAGE_NAME)) { + return damage.tagHandler().getTag(Tags.DAMAGE_NAME); } return null; diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageMobListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageMobListener.java index ebcb3316c..65a0c5429 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageMobListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerDamageMobListener.java @@ -25,7 +25,7 @@ public PlayerDamageMobListener(@NotNull Instance instance, @NotNull MobStore mob @Override public void accept(@NotNull PhantazmMob mob, @NotNull EntityDamageEvent event) { - if (!(event.getDamageType() instanceof EntityDamage entityDamage)) { + if (!(event.getDamage() instanceof EntityDamage entityDamage)) { return; } diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerInteractBlockListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerInteractBlockListener.java index 36a9c1d27..a9ad1076a 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerInteractBlockListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerInteractBlockListener.java @@ -3,6 +3,7 @@ import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerBlockInteractEvent; import net.minestom.server.instance.Instance; +import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.handler.DoorHandler; import org.phantazm.zombies.map.handler.ShopHandler; @@ -33,7 +34,10 @@ public PlayerInteractBlockListener(@NotNull Instance instance, @Override public void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull PlayerBlockInteractEvent event) { event.setCancelled(true); - event.setBlockingItemUse(true); + + if (event.getBlock().registry().material() != Material.CHEST) { + event.setBlockingItemUse(true); + } if (event.getHand() == Player.Hand.MAIN) { if (shopHandler.handleInteraction(zombiesPlayer, event.getBlockPosition(), diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerItemSelectListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerItemSelectListener.java index 4e1db4de0..2ffff44ae 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerItemSelectListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerItemSelectListener.java @@ -21,6 +21,11 @@ public PlayerItemSelectListener(@NotNull Instance instance, @Override protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull PlayerChangeHeldSlotEvent event) { + if (!zombiesPlayer.canUseEquipment()) { + event.setCancelled(true); + return; + } + InventoryAccessRegistry accessRegistry = zombiesPlayer.module().getInventoryAccessRegistry(); accessRegistry.getCurrentAccess().ifPresent(inventoryAccess -> { InventoryProfile profile = inventoryAccess.profile(); diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerLeftClickListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerLeftClickListener.java index a35908954..21e4b80a3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerLeftClickListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerLeftClickListener.java @@ -1,14 +1,16 @@ package org.phantazm.zombies.listener; +import net.minestom.server.MinecraftServer; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.Player; -import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.event.player.PlayerHandAnimationEvent; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; +import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; import org.phantazm.core.RayUtils; import org.phantazm.core.equipment.Equipment; @@ -27,14 +29,21 @@ public class PlayerLeftClickListener extends ZombiesPlayerEventListener lastPunchTag; public PlayerLeftClickListener(@NotNull Instance instance, @NotNull Map zombiesPlayers, @NotNull MobStore mobStore, - float punchDamage, float punchRange) { + float punchDamage, float punchRange, int punchCooldown, float punchKnockback) { super(instance, zombiesPlayers); this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); this.punchDamage = punchDamage; this.punchRange = punchRange; + this.lastPunchTag = Tag.Long("last_punch").defaultValue(0L); + this.punchCooldown = punchCooldown; + this.punchKnockback = punchKnockback; } @Override @@ -43,6 +52,10 @@ protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull PlayerHandA return; } + if (!zombiesPlayer.canUseEquipment()) { + return; + } + InventoryAccessRegistry profileSwitcher = zombiesPlayer.module().getInventoryAccessRegistry(); profileSwitcher.getCurrentAccess().ifPresent(inventoryAccess -> { InventoryProfile profile = inventoryAccess.profile(); @@ -66,8 +79,18 @@ private void handleNoEquipmentLeftClick(@NotNull ZombiesPlayer zombiesPlayer) { Instance instance = player.getInstance(); assert instance != null; + if (!zombiesPlayer.canDoGenericActions()) { + return; + } + boolean godmode = zombiesPlayer.flags().hasFlag(Flags.GODMODE); + long currentTime = 0L; + if (!godmode && ((currentTime = System.currentTimeMillis()) - player.getTag(lastPunchTag)) / + MinecraftServer.TICK_MS < punchCooldown) { + return; + } + Pos start = player.getPosition().add(0, player.getEyeHeight(), 0); Vec end = start.direction().mul(godmode ? 20 : punchRange); @@ -75,6 +98,10 @@ private void handleNoEquipmentLeftClick(@NotNull ZombiesPlayer zombiesPlayer) { instance.getEntityTracker() .raytraceCandidates(start, end, EntityTracker.Target.LIVING_ENTITIES, closestHit); + if (closestHit.closest > punchRange * punchRange) { + return; + } + PhantazmMob hit = closestHit.closestMob; if (hit != null) { LivingEntity entity = hit.entity(); @@ -83,7 +110,11 @@ private void handleNoEquipmentLeftClick(@NotNull ZombiesPlayer zombiesPlayer) { entity.kill(); } else { - entity.damage(DamageType.fromPlayer(player), punchDamage, false); + double angle = player.getPosition().yaw() * (Math.PI / 180); + + entity.damage(Damage.fromPlayer(player, punchDamage), false); + entity.takeKnockback(punchKnockback, Math.sin(angle), -Math.cos(angle)); + player.setTag(lastPunchTag, currentTime); } } }); diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerQuitListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerQuitListener.java index 76b8ec886..b3d75c995 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerQuitListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerQuitListener.java @@ -3,6 +3,7 @@ import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.game.scene.TransferResult; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.scene.LeaveHandler; @@ -23,7 +24,9 @@ public PlayerQuitListener(@NotNull Instance instance, @Override protected void accept(@NotNull ZombiesPlayer zombiesPlayer, @NotNull PlayerDisconnectEvent event) { - leaveHandler.leave(Collections.singleton(zombiesPlayer.module().getPlayerView().getUUID())).executor() - .ifPresent(Runnable::run); + try (TransferResult result = leaveHandler.leave( + Collections.singleton(zombiesPlayer.module().getPlayerView().getUUID()))) { + result.executor().ifPresent(Runnable::run); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerRightClickListener.java b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerRightClickListener.java index dc5a67505..ccb2e2fb4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/listener/PlayerRightClickListener.java +++ b/zombies/src/main/java/org/phantazm/zombies/listener/PlayerRightClickListener.java @@ -9,6 +9,10 @@ public class PlayerRightClickListener { public void onRightClick(@NotNull ZombiesPlayer player, int slot) { + if (!player.canUseEquipment()) { + return; + } + player.module().getInventoryAccessRegistry().getCurrentAccess().ifPresent(inventoryAccess -> { InventoryProfile profile = inventoryAccess.profile(); if (!profile.hasInventoryObject(slot)) { diff --git a/zombies/src/main/java/org/phantazm/zombies/map/Round.java b/zombies/src/main/java/org/phantazm/zombies/map/Round.java index a34d1946f..32b845624 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/Round.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/Round.java @@ -4,15 +4,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.phantazm.commons.Tickable; +import org.phantazm.core.packet.MinestomPacketUtils; +import org.phantazm.messaging.packet.server.RoundStartPacket; import org.phantazm.mob.PhantazmMob; import org.phantazm.zombies.map.action.Action; +import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.spawn.SpawnDistributor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; public class Round implements Tickable { private static final Logger LOGGER = LoggerFactory.getLogger(Round.class); @@ -23,8 +24,9 @@ public class Round implements Tickable { private final List> endActions; private final SpawnDistributor spawnDistributor; private final List spawnpoints; - private final List spawnedMobs; - + private final Map spawnedMobs; + private final Collection zombiesPlayers; + private boolean isActive; private long waveStartTime; private Wave currentWave; @@ -38,7 +40,7 @@ public class Round implements Tickable { */ public Round(@NotNull RoundInfo roundInfo, @NotNull List waves, @NotNull List> startActions, @NotNull List> endActions, @NotNull SpawnDistributor spawnDistributor, - @NotNull List spawnpoints) { + @NotNull List spawnpoints, @NotNull Collection zombiesPlayers) { List waveInfo = roundInfo.waves(); if (waveInfo.isEmpty()) { LOGGER.warn("Round {} has no waves", roundInfo); @@ -50,8 +52,9 @@ public Round(@NotNull RoundInfo roundInfo, @NotNull List waves, @NotNull L this.endActions = List.copyOf(endActions); this.spawnDistributor = Objects.requireNonNull(spawnDistributor, "spawnDistributor"); - this.spawnedMobs = new ArrayList<>(); + this.spawnedMobs = new HashMap<>(); this.spawnpoints = Objects.requireNonNull(spawnpoints, "spawnpoints"); + this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); } public @NotNull RoundInfo getRoundInfo() { @@ -59,23 +62,25 @@ public Round(@NotNull RoundInfo roundInfo, @NotNull List waves, @NotNull L } public void removeMob(@NotNull PhantazmMob mob) { - if (spawnedMobs.remove(mob)) { + if (spawnedMobs.remove(mob.entity().getUuid()) != null) { totalMobCount--; } } + public void addMob(@NotNull PhantazmMob mob) { + if (spawnedMobs.put(mob.entity().getUuid(), mob) == null) { + totalMobCount++; + } + } + public @Unmodifiable @NotNull List getSpawnedMobs() { - return List.copyOf(spawnedMobs); + return List.copyOf(spawnedMobs.values()); } public int getTotalMobCount() { return totalMobCount; } - public void setTotalMobCount(int count) { - this.totalMobCount = count; - } - public @Unmodifiable @NotNull List getWaves() { return waves; } @@ -90,6 +95,20 @@ public void startRound(long time) { } isActive = true; + + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + int prevBestRound = zombiesPlayer.module().getStats().getBestRound(); + zombiesPlayer.module().getStats().setBestRound(Math.max(prevBestRound, roundInfo.round())); + zombiesPlayer.getPlayer().ifPresent(player -> { + byte[] data = MinestomPacketUtils.serialize(new RoundStartPacket()); + player.sendPluginMessage(RoundStartPacket.ID.asString(), data); + }); + } + for (Action action : startActions) { action.perform(this); } @@ -122,13 +141,23 @@ public void endRound() { waveStartTime = 0; totalMobCount = 0; - spawnedMobs.clear(); + Iterator spawnedIterator = spawnedMobs.values().iterator(); + while (spawnedIterator.hasNext()) { + PhantazmMob mob = spawnedIterator.next(); + spawnedIterator.remove(); + + mob.entity().kill(); + } } public @NotNull List spawnMobs(@NotNull List spawnInfo) { return spawnMobs(spawnInfo, spawnDistributor, false); } + public boolean hasMob(@NotNull UUID uuid) { + return spawnedMobs.containsKey(uuid); + } + private @NotNull List spawnMobs(@NotNull List spawnInfo, @NotNull SpawnDistributor spawnDistributor, boolean isWave) { if (!isActive) { @@ -136,7 +165,9 @@ public void endRound() { } List spawns = spawnDistributor.distributeSpawns(spawnpoints, spawnInfo); - spawnedMobs.addAll(spawns); + for (PhantazmMob spawn : spawns) { + spawnedMobs.put(spawn.entity().getUuid(), spawn); + } if (isWave) { //adjust for mobs that may have failed to spawn diff --git a/zombies/src/main/java/org/phantazm/zombies/map/Window.java b/zombies/src/main/java/org/phantazm/zombies/map/Window.java index 5553db77e..d7349c295 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/Window.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/Window.java @@ -224,6 +224,23 @@ public long getLastBreakTime() { return Optional.ofNullable(linkedRoom); } + /** + * Checks if the block is currently broken. Accepts world coordinates. The behavior of this function is undefined + * if the position specified is outside of the bounds of this window. + * + * @param x the x-coordinate + * @param y the y-coordinate + * @param z the z-coordinate + * @return true if the block is broken, false otherwise + */ + public boolean isBlockBroken(int x, int y, int z) { + int index = coordinateToIndex(x, y, z); + + synchronized (sync) { + return index >= this.index; + } + } + private Point indexToCoordinate(int index) { Bounds3I frameRegion = windowInfo.frameRegion(); @@ -233,4 +250,15 @@ private Point indexToCoordinate(int index) { return worldMin.add(x, y, z); } + + private int coordinateToIndex(int x, int y, int z) { + int frameRelativeX = x - worldMin.blockX(); + int frameRelativeY = y - worldMin.blockY(); + int frameRelativeZ = z - worldMin.blockZ(); + + Bounds3I frameRegion = windowInfo.frameRegion(); + + return frameRelativeX + (frameRelativeY * frameRegion.lengthX()) + + (frameRelativeZ * frameRegion.lengthX() * frameRegion.lengthY()); + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorPlaySoundAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorPlaySoundAction.java index 57eb68c57..abe9844b9 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorPlaySoundAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorPlaySoundAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.door; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -11,6 +12,7 @@ import org.phantazm.zombies.map.action.Action; @Model("zombies.map.door.action.play_sound") +@Cache(false) public class DoorPlaySoundAction implements Action { private final Data data; private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendMessageAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendMessageAction.java index 75ed7c1e3..8a88ea795 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendMessageAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendMessageAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.door; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -15,6 +16,7 @@ import java.util.Optional; @Model("zombies.map.door.action.send_message") +@Cache(false) public class DoorSendMessageAction implements Action { private final Data data; private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendOpenedRoomsAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendOpenedRoomsAction.java index c3127f4c4..7cdd29307 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendOpenedRoomsAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/door/DoorSendOpenedRoomsAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.door; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -9,10 +10,12 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.TitlePart; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.ComponentUtils; +import org.phantazm.commons.MiniMessageUtils; import org.phantazm.zombies.map.Door; import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.action.Action; @@ -25,6 +28,7 @@ import java.util.function.Supplier; @Model("zombies.map.door.action.send_opened_rooms") +@Cache(false) public class DoorSendOpenedRoomsAction implements Action { private static final Logger LOGGER = LoggerFactory.getLogger(DoorSendOpenedRoomsAction.class); private static final Component UNKNOWN_COMPONENT = Component.text("..."); @@ -32,6 +36,7 @@ public class DoorSendOpenedRoomsAction implements Action { private final Data data; private final Supplier mapObjects; private final Instance instance; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); @FactoryMethod public DoorSendOpenedRoomsAction(@NotNull Data data, @NotNull Supplier mapObjects, @@ -45,25 +50,22 @@ public DoorSendOpenedRoomsAction(@NotNull Data data, @NotNull Supplier lastInteractorOptional = door.lastInteractor(); if (lastInteractorOptional.isPresent()) { - lastInteractorOptional.get().module().getPlayerView().getUsername().whenComplete((username, err) -> { - if (err != null) { - LOGGER.warn("Error resolving display name of door-opening player", err); - return; - } + lastInteractorOptional.get().module().getPlayerView().getDisplayName() + .whenComplete((displayName, throwable) -> { + if (throwable != null) { + LOGGER.warn("Error resolving display name of door-opening player", throwable); + return; + } - if (username != null) { - instance.sendTitlePart(data.nameTitlePart, - ComponentUtils.tryFormat(data.nameFormatString, username)); - } - else { - LOGGER.warn("Null username"); - instance.sendTitlePart(data.nameTitlePart, UNKNOWN_COMPONENT); - } - }); + TagResolver openerPlaceholder = Placeholder.component("opener", displayName); + instance.sendTitlePart(data.nameTitlePart, + miniMessage.deserialize(data.nameFormat, openerPlaceholder)); + }); } else { LOGGER.warn("Interacting player was null, cannot announce opener"); - instance.sendTitlePart(data.nameTitlePart, UNKNOWN_COMPONENT); + TagResolver openerPlaceholder = Placeholder.component("opener", UNKNOWN_COMPONENT); + instance.sendTitlePart(data.nameTitlePart, miniMessage.deserialize(data.nameFormat, openerPlaceholder)); } Map roomMap = mapObjects.get().roomMap(); @@ -76,30 +78,24 @@ public void perform(@NotNull Door door) { return !room.isOpen(); }).toList(); - StringBuilder builder = new StringBuilder(); - boolean appendedRoom = false; - for (int i = 0; i < opensTo.size(); i++) { - Room room = opensTo.get(i); - - builder.append(MiniMessage.miniMessage().serialize(room.getRoomInfo().displayName())); - appendedRoom = true; - - if (i < opensTo.size() - 1) { - builder.append(data.separator); - } + List roomNames = new ArrayList<>(opensTo.size()); + if (opensTo.isEmpty()) { + roomNames.add(Component.text("...")); } - - if (!appendedRoom) { - builder.append("..."); + else { + for (Room room : opensTo) { + roomNames.add(room.getRoomInfo().displayName()); + } } + TagResolver roomsPlaceholder = MiniMessageUtils.list("rooms", roomNames); instance.sendTitlePart(data.openedRoomsTitlePart, - ComponentUtils.tryFormat(data.openedRoomsFormatString, builder.toString())); + miniMessage.deserialize(data.openedRoomsFormat, roomsPlaceholder)); } @DataObject - public record Data(@NotNull String nameFormatString, - @NotNull String openedRoomsFormatString, + public record Data(@NotNull String nameFormat, + @NotNull String openedRoomsFormat, @NotNull String separator, @NotNull TitlePart nameTitlePart, @NotNull TitlePart openedRoomsTitlePart) { diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/room/SpawnMobsAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/room/SpawnMobsAction.java index 6c490517d..d48ea3002 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/room/SpawnMobsAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/room/SpawnMobsAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.room; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -18,6 +19,7 @@ import java.util.function.Supplier; @Model("zombies.map.room.action.spawn_mobs") +@Cache(false) public class SpawnMobsAction implements Action { private static final Logger LOGGER = LoggerFactory.getLogger(SpawnMobsAction.class); diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/round/AnnounceRoundAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/round/AnnounceRoundAction.java index 9781a8008..680d2b1f6 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/round/AnnounceRoundAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/round/AnnounceRoundAction.java @@ -1,29 +1,32 @@ package org.phantazm.zombies.map.action.round; -import com.github.steanky.element.core.annotation.DataObject; -import com.github.steanky.element.core.annotation.FactoryMethod; -import com.github.steanky.element.core.annotation.Model; -import com.github.steanky.ethylene.core.ConfigElement; -import com.github.steanky.ethylene.core.ConfigPrimitive; -import com.github.steanky.ethylene.mapper.annotation.Default; +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.toolkit.collection.Wrapper; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.TitlePart; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.time.TickFormatter; +import org.phantazm.commons.chat.ChatDestination; import org.phantazm.zombies.map.Round; import org.phantazm.zombies.map.action.Action; -import java.util.IllegalFormatException; import java.util.Objects; /** * An {@link Action} that announces the current round. */ @Model("zombies.map.round.action.announce") +@Cache(false) public class AnnounceRoundAction implements Action { private final Data data; private final Instance instance; + private final TickFormatter tickFormatter; + private final Wrapper ticksSinceStart; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); /** * Creates a new instance of this class from the provided contextual data. @@ -32,30 +35,34 @@ public class AnnounceRoundAction implements Action { * @param instance the current instance */ @FactoryMethod - public AnnounceRoundAction(@NotNull Data data, @NotNull Instance instance) { + public AnnounceRoundAction(@NotNull Data data, @NotNull Instance instance, @NotNull Wrapper ticksSinceStart, + @NotNull @Child("tick_formatter") TickFormatter tickFormatter) { this.data = Objects.requireNonNull(data, "data"); - this.instance = Objects.requireNonNull(instance, "instane"); + this.instance = Objects.requireNonNull(instance, "instance"); + this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); + this.ticksSinceStart = Objects.requireNonNull(ticksSinceStart, "ticksSinceStart"); } @Override public void perform(@NotNull Round round) { - String message = data.format ? data.formatMessage.formatted(round.getRoundInfo().round()) : data.formatMessage; - instance.sendTitlePart(data.titlePart, MiniMessage.miniMessage().deserialize(message)); + TagResolver roundPlaceholder = Placeholder.component("round", Component.text(round.getRoundInfo().round())); + String timeString = tickFormatter.format(ticksSinceStart.get()); + TagResolver timePlaceholder = Placeholder.unparsed("time", timeString); + + Component message = miniMessage.deserialize(data.format(), roundPlaceholder, timePlaceholder); + switch (data.destination()) { + case TITLE -> instance.sendTitlePart(TitlePart.TITLE, message); + case SUBTITLE -> instance.sendTitlePart(TitlePart.SUBTITLE, message); + case CHAT -> instance.sendMessage(message); + case ACTION_BAR -> instance.sendActionBar(message); + } } - /** - * Data for an AnnounceRoundAction. - * - * @param formatMessage the MiniMessage-compatible string. The format specifier %d will be replaced by the current - * round number (1-indexed) - * @param titlePart which Component-accepting {@link TitlePart} to send the message to - * @param format whether or not to format the message with the current round - */ @DataObject - public record Data(@NotNull String formatMessage, @NotNull TitlePart titlePart, boolean format) { - @Default("format") - public static ConfigElement formatDefault() { - return ConfigPrimitive.of(true); - } + public record Data(@NotNull String format, + @NotNull ChatDestination destination, + @NotNull @ChildPath("tick_formatter") String tickFormatter) { + } + } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/round/PlaySoundAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/round/PlaySoundAction.java index c928e2870..53408a6a3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/round/PlaySoundAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/round/PlaySoundAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.round; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -14,6 +15,7 @@ import java.util.UUID; @Model("zombies.map.round.action.play_sound") +@Cache(false) public class PlaySoundAction implements Action { private final Data data; private final Random random; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/round/RevivePlayersAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/round/RevivePlayersAction.java index dd0bb26aa..ac133243f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/round/RevivePlayersAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/round/RevivePlayersAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.round; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.minestom.server.coordinate.Pos; @@ -8,13 +9,14 @@ import org.phantazm.zombies.map.action.Action; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.state.ZombiesPlayerStateKeys; -import org.phantazm.zombies.player.state.context.NoContext; +import org.phantazm.zombies.player.state.context.AlivePlayerStateContext; import java.util.Map; import java.util.Objects; import java.util.UUID; @Model("zombies.map.round.action.revive_players") +@Cache(false) public class RevivePlayersAction implements Action { private final Map playerMap; private final Pos respawnPos; @@ -34,7 +36,7 @@ public void perform(@NotNull Round round) { zombiesPlayer.getPlayer().ifPresent(player -> player.teleport(respawnPos)); } - zombiesPlayer.setState(ZombiesPlayerStateKeys.ALIVE, NoContext.INSTANCE); + zombiesPlayer.setState(ZombiesPlayerStateKeys.ALIVE, AlivePlayerStateContext.regular()); } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/round/SelectBombedRoom.java b/zombies/src/main/java/org/phantazm/zombies/map/action/round/SelectBombedRoom.java index 698fd197d..d406ccd4c 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/round/SelectBombedRoom.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/round/SelectBombedRoom.java @@ -1,16 +1,22 @@ package org.phantazm.zombies.map.action.round; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import com.github.steanky.vector.Bounds3I; import com.github.steanky.vector.Vec3D; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.MinecraftServer; import net.minestom.server.attribute.Attribute; import net.minestom.server.attribute.AttributeModifier; import net.minestom.server.attribute.AttributeOperation; import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.damage.Damage; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Instance; import net.minestom.server.potion.Potion; @@ -22,7 +28,7 @@ import org.phantazm.core.particle.ParticleWrapper; import org.phantazm.zombies.Attributes; import org.phantazm.zombies.Flags; -import org.phantazm.zombies.damage.ZombiesDamageType; +import org.phantazm.zombies.Tags; import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.Round; import org.phantazm.zombies.map.action.Action; @@ -36,7 +42,7 @@ @Model("zombies.map.round.action.select_bombed") @Cache(false) public class SelectBombedRoom implements Action { - private static final Potion NAUSEA = new Potion(PotionEffect.NAUSEA, (byte)3, 360000); + private static final Potion NAUSEA = new Potion(PotionEffect.NAUSEA, (byte) 3, 360000); private final Data data; private final Supplier supplier; @@ -50,8 +56,8 @@ public class SelectBombedRoom implements Action { @FactoryMethod public SelectBombedRoom(@NotNull Data data, @NotNull Supplier supplier, - @NotNull Random random, @NotNull Instance instance, @NotNull @Child("particle") ParticleWrapper particle, - @NotNull Map playerMap) { + @NotNull Random random, @NotNull Instance instance, @NotNull @Child("particle") ParticleWrapper particle, + @NotNull Map playerMap) { this.data = data; this.supplier = supplier; this.random = random; @@ -81,13 +87,17 @@ public void perform(@NotNull Round round) { } Room room = candidateRooms.get(random.nextInt(candidateRooms.size())); - String serialized = MiniMessage.miniMessage().serialize(room.getRoomInfo().displayName()); + TagResolver roomPlaceholder = Placeholder.component("room", room.getRoomInfo().displayName()); Component warningMessage = - MiniMessage.miniMessage().deserialize(String.format(data.warningFormatMessage, serialized)); + MiniMessage.miniMessage().deserialize(data.warningFormatMessage, roomPlaceholder); instance.sendMessage(warningMessage); int startRoundIndex = objects.module().roundHandlerSupplier().get().currentRoundIndex(); + + Damage bombDamage = new Damage(DamageType.GENERIC, null, null, null, data.damage); + bombDamage.tagHandler().setTag(Tags.DAMAGE_NAME, data.bombingDamageName); + objects.taskScheduler().scheduleTaskAfter(new TickableTask() { private boolean flagSet; private int ticks; @@ -102,6 +112,7 @@ public boolean isFinished() { public void end() { for (ZombiesPlayer zombiesPlayer : playerMap.values()) { zombiesPlayer.removeCancellable(stateId); + zombiesPlayer.getPlayer().ifPresent(player -> player.removeTag(Tags.LAST_ENTER_BOMBED_ROOM)); } } @@ -115,24 +126,27 @@ public void tick(long time) { RoundHandler roundHandler = objects.module().roundHandlerSupplier().get(); int roundsElapsed = roundHandler.currentRoundIndex() - startRoundIndex; if (roundsElapsed >= data.duration) { + Component completionMessage = MiniMessage.miniMessage().deserialize(data.bombingCompleteFormat, roomPlaceholder); + instance.sendMessage(completionMessage); + room.flags().clearFlag(Flags.BOMBED_ROOM); flagSet = false; finished = true; for (ZombiesPlayer zombiesPlayer : playerMap.values()) { zombiesPlayer.getPlayer().ifPresent(player -> { + player.removeTag(Tags.LAST_ENTER_BOMBED_ROOM); + if (!zombiesPlayer.canDoGenericActions()) { zombiesPlayer.removeCancellable(stateId); - } - else { + } else { Optional roomOptional = objects.roomTracker().atPoint(player.getPosition()); if (roomOptional.isPresent()) { Room currentRoom = roomOptional.get(); if (!currentRoom.flags().hasFlag(Flags.BOMBED_ROOM)) { zombiesPlayer.removeCancellable(stateId); } - } - else { + } else { zombiesPlayer.removeCancellable(stateId); } } @@ -156,26 +170,38 @@ public void tick(long time) { if (roomOptional.isPresent()) { Room currentRoom = roomOptional.get(); if (currentRoom == room) { - applyModifiers(zombiesPlayer); - if (ticks % 40 == 0) { player.sendMessage(data.inAreaMessage); } - player.damage(ZombiesDamageType.BOMBING, data.damage, true); - } - else if (!currentRoom.flags().hasFlag(Flags.BOMBED_ROOM)) { + long lastEnterBombedRoom = player.getTag(Tags.LAST_ENTER_BOMBED_ROOM); + if (lastEnterBombedRoom == -1) { + player.setTag(Tags.LAST_ENTER_BOMBED_ROOM, time); + lastEnterBombedRoom = time; + } + + long ticksSinceEnter = (time - lastEnterBombedRoom) / MinecraftServer.TICK_MS; + + if (ticksSinceEnter >= data.damageDelay) { + player.damage(bombDamage, true); + } + + if (ticksSinceEnter >= data.effectDelay) { + applyModifiers(zombiesPlayer); + } + } else if (!currentRoom.flags().hasFlag(Flags.BOMBED_ROOM)) { zombiesPlayer.removeCancellable(stateId); + player.removeTag(Tags.LAST_ENTER_BOMBED_ROOM); } - } - else { + } else { zombiesPlayer.removeCancellable(stateId); + player.removeTag(Tags.LAST_ENTER_BOMBED_ROOM); } }); } } } - }, (long)data.gracePeriod * MinecraftServer.TICK_MS); + }, (long) data.gracePeriod * MinecraftServer.TICK_MS); } private void applyModifiers(ZombiesPlayer player) { @@ -243,12 +269,30 @@ private record TargetedAttribute(@NotNull String attribute, @NotNull AttributeMo @DataObject public record Data(@NotNull String warningFormatMessage, + @NotNull String bombingCompleteFormat, @NotNull Component inAreaMessage, + @NotNull Component bombingDamageName, float damage, int gracePeriod, + long damageDelay, + long effectDelay, int duration, @NotNull List exemptRooms, @NotNull List modifiers, @NotNull @ChildPath("particle") String particle) { + @Default("bombingDamageName") + public static @NotNull ConfigElement defaultBombingDamageName() { + return ConfigPrimitive.of("Bombing"); + } + + @Default("damageDelay") + public static @NotNull ConfigElement defaultDamageDelay() { + return ConfigPrimitive.of(100L); + } + + @Default("effectDelay") + public static @NotNull ConfigElement defaultEffectDelay() { + return ConfigPrimitive.of(50L); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/round/SpawnPowerupAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/round/SpawnPowerupAction.java index 0680d29f7..7496d9c25 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/round/SpawnPowerupAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/round/SpawnPowerupAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.round; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -17,6 +18,7 @@ import java.util.function.Supplier; @Model("zombies.map.room.action.spawn_powerup") +@Cache(false) public class SpawnPowerupAction implements Action { private static final Logger LOGGER = LoggerFactory.getLogger(SpawnPowerupAction.class); diff --git a/zombies/src/main/java/org/phantazm/zombies/map/action/wave/SelectPowerupZombieAction.java b/zombies/src/main/java/org/phantazm/zombies/map/action/wave/SelectPowerupZombieAction.java index e0e4f9330..6a99c71cd 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/action/wave/SelectPowerupZombieAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/action/wave/SelectPowerupZombieAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.action.wave; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -12,6 +13,7 @@ import java.util.*; @Model("zombies.map.wave.action.select_powerup") +@Cache(false) public class SelectPowerupZombieAction implements Action> { private final Data data; private final Random random; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicRoundHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicRoundHandler.java index 21744fb43..51f95aecb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicRoundHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicRoundHandler.java @@ -4,7 +4,10 @@ import org.phantazm.zombies.map.Round; import org.phantazm.zombies.player.ZombiesPlayer; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; public class BasicRoundHandler implements RoundHandler { private final Collection zombiesPlayers; @@ -26,24 +29,36 @@ public BasicRoundHandler(@NotNull Collection zombiesPla @Override public void tick(long time) { - if (currentRound != null) { - currentRound.tick(time); - - if (!currentRound.isActive()) { - for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { - zombiesPlayer.module().getStats() - .setRoundsSurvived(zombiesPlayer.module().getStats().getRoundsSurvived() + 1); - } - - if (++roundIndex < rounds.size()) { - currentRound = rounds.get(roundIndex); - currentRound.startRound(time); - } - else { - hasEnded = true; - currentRound = null; - } + boolean hasEnded = this.hasEnded; + Round currentRound = this.currentRound; + + if (currentRound == null || hasEnded) { + return; + } + + currentRound.tick(time); + if (currentRound.isActive()) { + return; + } + + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (zombiesPlayer.hasQuit()) { + continue; } + + zombiesPlayer.module().getStats() + .setRoundsSurvived(zombiesPlayer.module().getStats().getRoundsSurvived() + 1); + } + + if (++roundIndex < rounds.size()) { + currentRound = rounds.get(roundIndex); + currentRound.startRound(time); + + this.currentRound = currentRound; + } + else { + this.hasEnded = true; + this.currentRound = null; } } @@ -54,22 +69,19 @@ public int roundCount() { @Override public int currentRoundIndex() { - return roundIndex; + return Math.min(roundIndex, rounds.size() - 1); } @Override public void setCurrentRound(int roundIndex) { Objects.checkIndex(roundIndex, rounds.size()); - this.roundIndex = roundIndex; - Round newCurrent = rounds.get(roundIndex); - if (newCurrent == currentRound) { + if (currentRound != null) { currentRound.endRound(); - currentRound.startRound(System.currentTimeMillis()); - return; } - currentRound = newCurrent; + this.roundIndex = roundIndex; + currentRound = rounds.get(roundIndex); currentRound.startRound(System.currentTimeMillis()); } @@ -82,4 +94,10 @@ public void setCurrentRound(int roundIndex) { public boolean hasEnded() { return hasEnded; } + + @Override + public void end() { + hasEnded = true; + currentRound = null; + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandler.java index 47255a586..665324cb8 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandler.java @@ -6,17 +6,21 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.tracker.BoundedTracker; import org.phantazm.zombies.map.BasicPlayerInteraction; +import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.stage.StageKeys; import java.util.Objects; +import java.util.Optional; public class BasicShopHandler implements ShopHandler { private final BoundedTracker shopTracker; + private final BoundedTracker roomTracker; - public BasicShopHandler(@NotNull BoundedTracker shopTracker) { + public BasicShopHandler(@NotNull BoundedTracker shopTracker, @NotNull BoundedTracker roomTracker) { this.shopTracker = Objects.requireNonNull(shopTracker, "shopTracker"); + this.roomTracker = Objects.requireNonNull(roomTracker, "roomTracker"); } @Override @@ -34,6 +38,13 @@ public boolean handleInteraction(@NotNull ZombiesPlayer player, @NotNull Point c Wrapper result = Wrapper.of(false); shopTracker.atPoint(clicked).ifPresent(shop -> { + Optional roomOptional = roomTracker.atPoint(shop.center()); + if (roomOptional.isPresent()) { + if (!roomOptional.get().isOpen()) { + return; + } + } + shop.handleInteraction(new BasicPlayerInteraction(player, interactionType)); result.set(true); }); diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandlerSource.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandlerSource.java index 80b7294e9..74f916fb0 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandlerSource.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicShopHandlerSource.java @@ -2,11 +2,12 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.tracker.BoundedTracker; +import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.shop.Shop; public class BasicShopHandlerSource implements ShopHandler.Source { @Override - public @NotNull ShopHandler make(@NotNull BoundedTracker shops) { - return new BasicShopHandler(shops); + public @NotNull ShopHandler make(@NotNull BoundedTracker shops, @NotNull BoundedTracker rooms) { + return new BasicShopHandler(shops, rooms); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandler.java index feb232c33..cfa57f6ed 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandler.java @@ -1,10 +1,7 @@ package org.phantazm.zombies.map.handler; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import org.jetbrains.annotations.NotNull; @@ -22,7 +19,7 @@ public class BasicWindowHandler implements WindowHandler { private static final int POSITION_CHECK_INTERVAL = 200; - private static final int UNREPAIRABLE_BREAK_DELAY = 1000; + private static final int UNREPAIRABLE_BREAK_DELAY = 2000; private static class RepairOperation { private final ZombiesPlayer zombiesPlayer; @@ -45,11 +42,13 @@ private RepairOperation(ZombiesPlayer zombiesPlayer, Window window, long lastRep private final Map repairOperationMap; private final Collection activeRepairs; + private final WindowMessages windowMessages; + private long lastPositionCheck; public BasicWindowHandler(@NotNull BoundedTracker windowTracker, @NotNull Collection players, double repairRadius, long repairInterval, - int coinsPerWindowBlock) { + int coinsPerWindowBlock, @NotNull WindowMessages windowMessages) { this.windowTracker = Objects.requireNonNull(windowTracker, "windowTracker"); this.players = Objects.requireNonNull(players, "players"); this.repairRadius = repairRadius; @@ -58,12 +57,23 @@ public BasicWindowHandler(@NotNull BoundedTracker windowTracker, this.activeRepairs = repairOperationMap.values(); this.coinsPerWindowBlock = coinsPerWindowBlock; this.lastPositionCheck = System.currentTimeMillis(); + this.windowMessages = Objects.requireNonNull(windowMessages, "windowMessages"); } @Override public void handleCrouchStateChange(@NotNull ZombiesPlayer zombiesPlayer, boolean crouching) { Optional playerOptional = zombiesPlayer.getPlayer(); + if (!crouching && playerOptional.isPresent() && zombiesPlayer.canRepairWindow() && + zombiesPlayer.inStage(StageKeys.IN_GAME)) { + RepairOperation repairOperation = repairOperationMap.remove(zombiesPlayer.getUUID()); + if (repairOperation != null && !repairOperation.window.isFullyRepaired()) { + zombiesPlayer.sendMessage(windowMessages.stopRepairing()); + } + + return; + } + if (!crouching || playerOptional.isEmpty() || !zombiesPlayer.canRepairWindow() || !zombiesPlayer.inStage(StageKeys.IN_GAME)) { repairOperationMap.remove(zombiesPlayer.getUUID()); @@ -79,9 +89,15 @@ private void addOperationIfNearby(ZombiesPlayer zombiesPlayer, Player player) { double width = boundingBox.width(); double height = boundingBox.height(); - windowTracker.closestInRangeToBounds(player.getPosition(), width, height, repairRadius).ifPresent( - window -> repairOperationMap.putIfAbsent(player.getUuid(), - new RepairOperation(zombiesPlayer, window, System.currentTimeMillis()))); + windowTracker.closestInRangeToBounds(player.getPosition(), width, height, repairRadius).ifPresent(window -> { + repairOperationMap.computeIfAbsent(player.getUuid(), ignored -> { + if (!window.isFullyRepaired()) { + player.sendMessage(windowMessages.startRepairing()); + } + + return new RepairOperation(zombiesPlayer, window, System.currentTimeMillis()); + }); + }); } @Override @@ -97,9 +113,22 @@ public void tick(long time) { if (playerOptional.isPresent() && zombiesPlayer.canRepairWindow()) { Player player = playerOptional.get(); - if (player.getPose() == Entity.Pose.SNEAKING) { + if (player.isSneaking()) { addOperationIfNearby(zombiesPlayer, player); } + else { + BoundingBox boundingBox = player.getBoundingBox(); + double width = boundingBox.width(); + double height = boundingBox.height(); + + Optional windowOptional = + windowTracker.closestInRangeToBounds(player.getPosition(), width, height, repairRadius); + if (windowOptional.isPresent()) { + if (!windowOptional.get().isFullyRepaired()) { + player.sendActionBar(windowMessages.nearWindow()); + } + } + } } } @@ -147,8 +176,7 @@ public void tick(long time) { if (!targetWindow.isFullyRepaired()) { long timeElapsedSinceLastBroken = time - targetWindow.getLastBreakTime(); if (timeElapsedSinceLastBroken < UNREPAIRABLE_BREAK_DELAY) { - zombiesPlayer.sendMessage(Component.text("You can't repair windows while enemies are nearby!") - .color(NamedTextColor.RED)); + zombiesPlayer.sendMessage(windowMessages.enemiesNearby()); continue; } @@ -160,6 +188,7 @@ public void tick(long time) { Optional playerOptional = zombiesPlayer.getPlayer(); if (playerOptional.isEmpty()) { + repairOperationIterator.remove(); continue; } @@ -181,7 +210,10 @@ public void tick(long time) { continue; } - coins.applyTransaction(new TransactionResult(result.modifierNames(), event.goldGain())); + coins.applyTransaction(new TransactionResult(result.displays(), event.goldGain())); + if (targetWindow.isFullyRepaired()) { + zombiesPlayer.sendMessage(windowMessages.finishRepairing()); + } } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandlerSource.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandlerSource.java index a49416c75..246f2b3b5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandlerSource.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/BasicWindowHandlerSource.java @@ -6,21 +6,26 @@ import org.phantazm.zombies.player.ZombiesPlayer; import java.util.Collection; +import java.util.Objects; public class BasicWindowHandlerSource implements WindowHandler.Source { private final double repairRadius; private final long repairInterval; private final int coinsPerWindowBlock; + private final WindowHandler.WindowMessages windowMessages; - public BasicWindowHandlerSource(double repairRadius, long repairInterval, int coinsPerWindowBlock) { + public BasicWindowHandlerSource(double repairRadius, long repairInterval, int coinsPerWindowBlock, + @NotNull WindowHandler.WindowMessages windowMessages) { this.repairRadius = repairRadius; this.repairInterval = repairInterval; this.coinsPerWindowBlock = coinsPerWindowBlock; + this.windowMessages = Objects.requireNonNull(windowMessages, "windowMessages"); } @Override public @NotNull WindowHandler make(@NotNull BoundedTracker windowTracker, @NotNull Collection players) { - return new BasicWindowHandler(windowTracker, players, repairRadius, repairInterval, coinsPerWindowBlock); + return new BasicWindowHandler(windowTracker, players, repairRadius, repairInterval, coinsPerWindowBlock, + windowMessages); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/RoundHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/RoundHandler.java index ea9143b68..bc2b9cb6d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/RoundHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/RoundHandler.java @@ -16,4 +16,6 @@ public interface RoundHandler extends Tickable { @NotNull Optional currentRound(); boolean hasEnded(); + + void end(); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/ShopHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/ShopHandler.java index 438339852..79a9ad733 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/ShopHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/ShopHandler.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.commons.Tickable; import org.phantazm.core.tracker.BoundedTracker; +import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.player.ZombiesPlayer; @@ -17,6 +18,6 @@ public interface ShopHandler extends Tickable { void initialize(); interface Source { - @NotNull ShopHandler make(@NotNull BoundedTracker shops); + @NotNull ShopHandler make(@NotNull BoundedTracker shops, @NotNull BoundedTracker rooms); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/handler/WindowHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/handler/WindowHandler.java index 21ba2e457..910154e34 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/handler/WindowHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/handler/WindowHandler.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.handler; +import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Tickable; import org.phantazm.core.tracker.BoundedTracker; @@ -17,4 +18,12 @@ interface Source { @NotNull WindowHandler make(@NotNull BoundedTracker windowTracker, @NotNull Collection players); } + + record WindowMessages(@NotNull Component nearWindow, + @NotNull Component startRepairing, + @NotNull Component stopRepairing, + @NotNull Component finishRepairing, + @NotNull Component enemiesNearby) { + + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjects.java b/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjects.java index eb6233145..25dd58beb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjects.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjects.java @@ -3,7 +3,9 @@ import com.github.steanky.element.core.dependency.DependencyProvider; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; +import net.minestom.server.scoreboard.Team; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.phantazm.commons.TickTaskScheduler; import org.phantazm.core.tracker.BoundedTracker; @@ -31,11 +33,15 @@ public final class BasicMapObjects implements MapObjects { private final TickTaskScheduler taskScheduler; + private final Team mobNoPushTeam; + private final Team corpseTeam; + public BasicMapObjects(@NotNull List spawnpoints, @NotNull BoundedTracker windows, @NotNull BoundedTracker shops, @NotNull BoundedTracker doors, @NotNull BoundedTracker rooms, @NotNull List rounds, @NotNull DependencyProvider mapDependencyProvider, @NotNull MobSpawner mobSpawner, @NotNull Point mapOrigin, - @NotNull Module module, @NotNull TickTaskScheduler taskScheduler) { + @NotNull Module module, @NotNull TickTaskScheduler taskScheduler, @Nullable Team mobNoPushTeam, + @NotNull Team corpseTeam) { this.spawnpoints = List.copyOf(spawnpoints); this.rounds = List.copyOf(rounds); @@ -57,6 +63,8 @@ public BasicMapObjects(@NotNull List spawnpoints, @NotNull BoundedTr this.roomMap = Map.copyOf(map); this.mapOrigin = mapOrigin; this.taskScheduler = Objects.requireNonNull(taskScheduler, "taskScheduler"); + this.mobNoPushTeam = mobNoPushTeam; + this.corpseTeam = Objects.requireNonNull(corpseTeam, "corpseTeam"); } @Override @@ -118,4 +126,14 @@ public BasicMapObjects(@NotNull List spawnpoints, @NotNull BoundedTr public @NotNull TickTaskScheduler taskScheduler() { return taskScheduler; } + + @Override + public @Nullable Team mobNoPushTeam() { + return mobNoPushTeam; + } + + @Override + public @NotNull Team corpseTeam() { + return corpseTeam; + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjectsSource.java b/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjectsSource.java index 3f2e301b4..281eba0c5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjectsSource.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/objects/BasicMapObjectsSource.java @@ -28,18 +28,22 @@ import org.phantazm.core.VecUtils; import org.phantazm.core.gui.BasicSlotDistributor; import org.phantazm.core.gui.SlotDistributor; +import org.phantazm.core.sound.SongLoader; import org.phantazm.core.sound.SongPlayer; import org.phantazm.core.tracker.BoundedTracker; import org.phantazm.mob.MobModel; import org.phantazm.mob.MobStore; import org.phantazm.mob.PhantazmMob; import org.phantazm.mob.spawner.MobSpawner; +import org.phantazm.zombies.Flags; import org.phantazm.zombies.coin.BasicTransactionModifierSource; import org.phantazm.zombies.coin.TransactionModifierSource; import org.phantazm.zombies.map.*; import org.phantazm.zombies.map.action.Action; import org.phantazm.zombies.map.handler.RoundHandler; import org.phantazm.zombies.map.handler.WindowHandler; +import org.phantazm.zombies.map.shop.BasicInteractorGroupHandler; +import org.phantazm.zombies.map.shop.InteractorGroupHandler; import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.display.ShopDisplay; import org.phantazm.zombies.map.shop.interactor.ShopInteractor; @@ -54,6 +58,7 @@ import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public class BasicMapObjectsSource implements MapObjects.Source { @@ -84,13 +89,18 @@ public BasicMapObjectsSource(@NotNull MapInfo mapInfo, @NotNull ContextManager c @NotNull Supplier roundHandlerSupplier, @NotNull MobStore mobStore, @Nullable Team mobNoPushTeam, @NotNull Wrapper powerupHandler, @NotNull Wrapper windowHandler, @NotNull Wrapper> eventNode, - @NotNull SongPlayer songPlayer, @NotNull TickTaskScheduler tickTaskScheduler, @NotNull Team corpseTeam) { + @NotNull SongPlayer songPlayer, @NotNull SongLoader songLoader, + @NotNull TickTaskScheduler tickTaskScheduler, @NotNull Team corpseTeam, + @NotNull Wrapper ticksSinceStart) { Random random = new Random(); ClientBlockHandler clientBlockHandler = clientBlockHandlerSource.forInstance(instance); SpawnDistributor spawnDistributor = new BasicSpawnDistributor(mobModels::get, random, playerMap.values(), mobNoPushTeam); Flaggable flaggable = new BasicFlaggable(); + if (mapInfo.settings().canWallshoot()) { + flaggable.setFlag(Flags.WALLSHOOTING_ENABLED); + } TransactionModifierSource transactionModifierSource = new BasicTransactionModifierSource(); SlotDistributor slotDistributor = new BasicSlotDistributor(1); @@ -105,7 +115,8 @@ public BasicMapObjectsSource(@NotNull MapInfo mapInfo, @NotNull ContextManager c Module module = new Module(keyParser, instance, random, roundHandlerSupplier, flaggable, transactionModifierSource, slotDistributor, playerMap, respawnPos, mapObjectsWrapper, powerupHandler, windowHandler, - eventNode, mobStore, songPlayer, corpseTeam); + eventNode, mobStore, songPlayer, songLoader, corpseTeam, new BasicInteractorGroupHandler(), + ticksSinceStart, mobModels::get); DependencyProvider provider = new ModuleDependencyProvider(keyParser, module); @@ -130,11 +141,11 @@ public BasicMapObjectsSource(@NotNull MapInfo mapInfo, @NotNull ContextManager c buildSpawnpoints(origin, mapInfo.spawnpoints(), spawnruleInfoMap, instance, mobSpawner, windowTracker, roomTracker); - List rounds = buildRounds(mapInfo.rounds(), spawnpoints, provider, spawnDistributor); + List rounds = buildRounds(mapInfo.rounds(), spawnpoints, provider, spawnDistributor, playerMap); MapObjects mapObjects = new BasicMapObjects(spawnpoints, windowTracker, shopTracker, doorTracker, roomTracker, rounds, provider, - mobSpawner, origin, module, tickTaskScheduler); + mobSpawner, origin, module, tickTaskScheduler, mobNoPushTeam, corpseTeam); mapObjectsWrapper.set(mapObjects); return mapObjects; @@ -237,7 +248,8 @@ private List buildRooms(Point mapOrigin, List roomInfoList, Depe } private List buildRounds(List roundInfoList, List spawnpoints, - DependencyProvider dependencyProvider, SpawnDistributor spawnDistributor) { + DependencyProvider dependencyProvider, SpawnDistributor spawnDistributor, + Map zombiesPlayers) { List rounds = new ArrayList<>(roundInfoList.size()); for (RoundInfo roundInfo : roundInfoList) { List> startActions = contextManager.makeContext(roundInfo.startActions()) @@ -255,7 +267,8 @@ private List buildRounds(List roundInfoList, List waves.add(new Wave(wave, spawnActions)); } - rounds.add(new Round(roundInfo, waves, startActions, endActions, spawnDistributor, spawnpoints)); + rounds.add(new Round(roundInfo, waves, startActions, endActions, spawnDistributor, spawnpoints, + zombiesPlayers.values())); } return rounds; @@ -279,7 +292,11 @@ public static class Module implements DependencyModule, MapObjects.Module { private final Wrapper> eventNode; private final MobStore mobStore; private final SongPlayer songPlayer; + private final SongLoader songLoader; private final Team corpseTeam; + private final InteractorGroupHandler interactorGroupHandler; + private final Wrapper ticksSinceStart; + private final Function mobModelFunction; private Module(KeyParser keyParser, Instance instance, Random random, Supplier roundHandlerSupplier, Flaggable flaggable, @@ -287,7 +304,9 @@ private Module(KeyParser keyParser, Instance instance, Random random, Map playerMap, Pos respawnPos, Supplier mapObjectsSupplier, Wrapper powerupHandler, Wrapper windowHandler, Wrapper> eventNode, MobStore mobStore, - SongPlayer songPlayer, Team corpseTeam) { + SongPlayer songPlayer, SongLoader songLoader, Team corpseTeam, + InteractorGroupHandler interactorGroupHandler, Wrapper ticksSinceStart, + Function mobModelFunction) { this.keyParser = Objects.requireNonNull(keyParser, "keyParser"); this.instance = Objects.requireNonNull(instance, "instance"); this.random = Objects.requireNonNull(random, "random"); @@ -303,7 +322,11 @@ private Module(KeyParser keyParser, Instance instance, Random random, this.eventNode = Objects.requireNonNull(eventNode, "eventNode"); this.mobStore = Objects.requireNonNull(mobStore, "mobStore"); this.songPlayer = Objects.requireNonNull(songPlayer, "songPlayer"); + this.songLoader = Objects.requireNonNull(songLoader, "songLoader"); this.corpseTeam = Objects.requireNonNull(corpseTeam, "corpseTeam"); + this.interactorGroupHandler = Objects.requireNonNull(interactorGroupHandler, "interactorGroupHandler"); + this.ticksSinceStart = Objects.requireNonNull(ticksSinceStart, "ticksSinceStart"); + this.mobModelFunction = Objects.requireNonNull(mobModelFunction, "mobModelFunction"); } @Override @@ -386,9 +409,29 @@ private Module(KeyParser keyParser, Instance instance, Random random, return songPlayer; } + @Override + public @NotNull SongLoader songLoader() { + return songLoader; + } + @Override public @NotNull Team corpseTeam() { return corpseTeam; } + + @Override + public @NotNull InteractorGroupHandler interactorGroupHandler() { + return interactorGroupHandler; + } + + @Override + public @NotNull Wrapper ticksSinceStart() { + return ticksSinceStart; + } + + @Override + public @NotNull Function mobModelFunction() { + return mobModelFunction; + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/objects/MapObjects.java b/zombies/src/main/java/org/phantazm/zombies/map/objects/MapObjects.java index 71ca427d5..769a2a765 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/objects/MapObjects.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/objects/MapObjects.java @@ -15,19 +15,23 @@ import org.jetbrains.annotations.Unmodifiable; import org.phantazm.commons.TickTaskScheduler; import org.phantazm.core.gui.SlotDistributor; +import org.phantazm.core.sound.SongLoader; import org.phantazm.core.sound.SongPlayer; import org.phantazm.core.tracker.BoundedTracker; +import org.phantazm.mob.MobModel; import org.phantazm.mob.MobStore; import org.phantazm.mob.spawner.MobSpawner; import org.phantazm.zombies.coin.TransactionModifierSource; import org.phantazm.zombies.map.*; import org.phantazm.zombies.map.handler.RoundHandler; import org.phantazm.zombies.map.handler.WindowHandler; +import org.phantazm.zombies.map.shop.InteractorGroupHandler; import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.powerup.PowerupHandler; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; public interface MapObjects { @@ -55,13 +59,19 @@ public interface MapObjects { @NotNull TickTaskScheduler taskScheduler(); + @Nullable Team mobNoPushTeam(); + + @NotNull Team corpseTeam(); + @NotNull interface Source { @NotNull MapObjects make(@NotNull Instance instance, @NotNull Map playerMap, @NotNull Supplier roundHandlerSupplier, @NotNull MobStore mobStore, @Nullable Team mobNoPushTeam, @NotNull Wrapper powerupHandler, @NotNull Wrapper windowHandler, @NotNull Wrapper> eventNode, - @NotNull SongPlayer songPlayer, @NotNull TickTaskScheduler tickTaskScheduler, @NotNull Team corpseTeam); + @NotNull SongPlayer songPlayer, @NotNull SongLoader songLoader, + @NotNull TickTaskScheduler tickTaskScheduler, @NotNull Team corpseTeam, + @NotNull Wrapper ticksSinceStart); } interface Module { @@ -95,8 +105,16 @@ interface Module { @NotNull MobStore mobStore(); + @NotNull SongLoader songLoader(); + @NotNull SongPlayer songPlayer(); @NotNull Team corpseTeam(); + + @NotNull InteractorGroupHandler interactorGroupHandler(); + + @NotNull Wrapper ticksSinceStart(); + + @NotNull Function mobModelFunction(); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/BasicInteractorGroupHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/BasicInteractorGroupHandler.java new file mode 100644 index 000000000..ff030ec8a --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/BasicInteractorGroupHandler.java @@ -0,0 +1,46 @@ +package org.phantazm.zombies.map.shop; + +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.phantazm.zombies.map.shop.interactor.SelectionGroupInteractor; + +import java.util.*; + +public class BasicInteractorGroupHandler implements InteractorGroupHandler { + private final Map> interactorGroups; + private final Object2BooleanMap chosenMap; + + public BasicInteractorGroupHandler() { + this.interactorGroups = new HashMap<>(); + this.chosenMap = new Object2BooleanOpenHashMap<>(); + } + + @Override + public void subscribe(@NotNull Key group, @NotNull SelectionGroupInteractor interactor) { + Objects.requireNonNull(group, "group"); + Objects.requireNonNull(interactor, "interactor"); + + interactorGroups.computeIfAbsent(group, ignored -> new ArrayList<>()).add(interactor); + } + + @Override + public @NotNull @Unmodifiable List interactors(@NotNull Key group) { + Objects.requireNonNull(group, "group"); + return interactorGroups.getOrDefault(group, List.of()); + } + + @Override + public void markChosen(@NotNull Key group) { + Objects.requireNonNull(group, "group"); + chosenMap.put(group, true); + } + + @Override + public boolean isChosen(@NotNull Key group) { + Objects.requireNonNull(group, "group"); + return chosenMap.getOrDefault(group, false); + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/InteractorGroupHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/InteractorGroupHandler.java new file mode 100644 index 000000000..46dbf021a --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/InteractorGroupHandler.java @@ -0,0 +1,18 @@ +package org.phantazm.zombies.map.shop; + +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.phantazm.zombies.map.shop.interactor.SelectionGroupInteractor; + +import java.util.List; + +public interface InteractorGroupHandler { + void subscribe(@NotNull Key group, @NotNull SelectionGroupInteractor interactor); + + @NotNull @Unmodifiable List interactors(@NotNull Key group); + + void markChosen(@NotNull Key group); + + boolean isChosen(@NotNull Key group); +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/Shop.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/Shop.java index febc73cc8..1c58d78be 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/Shop.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/Shop.java @@ -3,11 +3,13 @@ import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; +import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.phantazm.commons.Tickable; import org.phantazm.core.tracker.BoundedBase; -import org.phantazm.zombies.Tags; +import org.phantazm.zombies.map.BasicFlaggable; +import org.phantazm.zombies.map.Flaggable; import org.phantazm.zombies.map.ShopInfo; import org.phantazm.zombies.map.shop.display.ShopDisplay; import org.phantazm.zombies.map.shop.interactor.ShopInteractor; @@ -16,9 +18,10 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.UUID; public class Shop extends BoundedBase implements Tickable { - public static final long SHOP_ACTIVATION_DELAY = 500L; + public static final long SHOP_ACTIVATION_DELAY = 750L; private final Point mapOrigin; private final Instance instance; @@ -29,6 +32,10 @@ public class Shop extends BoundedBase implements Tickable { private final List failureInteractors; private final List displays; + private final Tag lastActivationTag; + + private final Flaggable flaggable; + public Shop(@NotNull Point mapOrigin, @NotNull ShopInfo shopInfo, @NotNull Instance instance, @NotNull List predicates, @NotNull List successInteractors, @NotNull List failureInteractors, @NotNull List displays) { @@ -42,6 +49,9 @@ public Shop(@NotNull Point mapOrigin, @NotNull ShopInfo shopInfo, @NotNull Insta this.successInteractors = List.copyOf(successInteractors); this.failureInteractors = List.copyOf(failureInteractors); this.displays = List.copyOf(displays); + + this.lastActivationTag = Tag.Long(UUID.randomUUID().toString()).defaultValue(-1L); + this.flaggable = new BasicFlaggable(); } public @NotNull Point mapOrigin() { @@ -86,18 +96,22 @@ public void initialize() { } } + public @NotNull Flaggable flags() { + return flaggable; + } + public void handleInteraction(@NotNull PlayerInteraction interaction) { Optional playerOptional = interaction.player().getPlayer(); if (playerOptional.isPresent()) { Player player = playerOptional.get(); - long lastActivate = player.getTag(Tags.LAST_SHOP_ACTIVATE); + long lastActivate = player.getTag(lastActivationTag); if (lastActivate != -1 && System.currentTimeMillis() - lastActivate < SHOP_ACTIVATION_DELAY) { return; } } - boolean success = shopInfo.predicateEvaluation().evaluate(predicates, interaction); + boolean success = shopInfo.predicateEvaluation().evaluate(predicates, interaction, this); List interactorsToCall = success ? successInteractors : failureInteractors; for (ShopInteractor interactor : interactorsToCall) { @@ -108,7 +122,7 @@ public void handleInteraction(@NotNull PlayerInteraction interaction) { display.update(this, interaction, success); } - playerOptional.ifPresent(player -> player.setTag(Tags.LAST_SHOP_ACTIVATE, System.currentTimeMillis())); + playerOptional.ifPresent(player -> player.setTag(lastActivationTag, System.currentTimeMillis())); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/ShopFlagSource.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/ShopFlagSource.java new file mode 100644 index 000000000..befa59d3d --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/ShopFlagSource.java @@ -0,0 +1,6 @@ +package org.phantazm.zombies.map.shop; + +public enum ShopFlagSource { + MAP, + SHOP +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/FlagConditionalDisplay.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/FlagConditionalDisplay.java index a2dca7a46..ea2563c77 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/FlagConditionalDisplay.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/FlagConditionalDisplay.java @@ -1,9 +1,13 @@ package org.phantazm.zombies.map.shop.display; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.Flaggable; +import org.phantazm.zombies.map.shop.ShopFlagSource; import org.phantazm.zombies.map.shop.PlayerInteraction; import org.phantazm.zombies.map.shop.Shop; @@ -33,20 +37,22 @@ public FlagConditionalDisplay(@NotNull Data data, @NotNull @Child("success") Lis @Override public void initialize(@NotNull Shop shop) { this.shop = shop; - if (flags.hasFlag(data.flag)) { - current = success; - } - else { - current = failure; - } + + List current = switch (data.source) { + case MAP -> flags.hasFlag(data.flag) ? success : failure; + case SHOP -> shop.flags().hasFlag(data.flag) ? success : failure; + }; for (ShopDisplay display : current) { display.initialize(shop); } + + this.current = current; } @Override public void destroy(@NotNull Shop shop) { + List current = this.current; if (current == null) { return; } @@ -58,6 +64,7 @@ public void destroy(@NotNull Shop shop) { @Override public void update(@NotNull Shop shop, @NotNull PlayerInteraction interaction, boolean interacted) { + List current = this.current; if (current == null) { return; } @@ -69,20 +76,14 @@ public void update(@NotNull Shop shop, @NotNull PlayerInteraction interaction, b @Override public void tick(long time) { + Shop shop = this.shop; if (shop == null) { return; } - if (ticks++ % 20 == 0) { + if (ticks++ % 4 == 0) { List oldCurrent = current; - - List newCurrent; - if (flags.hasFlag(data.flag)) { - newCurrent = success; - } - else { - newCurrent = failure; - } + List newCurrent = hasFlag() ? success : failure; if (newCurrent != oldCurrent) { if (oldCurrent != null) { @@ -98,6 +99,7 @@ public void tick(long time) { } } + List current = this.current; if (current != null) { for (ShopDisplay display : current) { display.tick(time); @@ -105,9 +107,22 @@ public void tick(long time) { } } + private boolean hasFlag() { + Shop shop = this.shop; + return switch (data.source) { + case MAP -> flags.hasFlag(data.flag); + case SHOP -> shop != null && shop.flags().hasFlag(data.flag); + }; + } + @DataObject public record Data(@NotNull Key flag, + @NotNull ShopFlagSource source, @NotNull @ChildPath("success") List success, @NotNull @ChildPath("failure") List failure) { + @Default("source") + public static @NotNull ConfigElement defaultShopFlagSource() { + return ConfigPrimitive.of("MAP"); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/CombiningArmorPlayerDisplayCreator.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/CombiningArmorPlayerDisplayCreator.java index 1a561e921..b382c71c4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/CombiningArmorPlayerDisplayCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/CombiningArmorPlayerDisplayCreator.java @@ -7,21 +7,17 @@ import com.github.steanky.vector.Vec3D; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.EntityType; -import net.minestom.server.entity.EquipmentSlot; -import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.*; import net.minestom.server.entity.metadata.other.ArmorStandMeta; import net.minestom.server.item.ItemStack; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; -import org.phantazm.zombies.Tags; import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.display.ShopDisplay; import org.phantazm.zombies.player.ZombiesPlayer; -import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Model("zombies.map.shop.display.player.combining_armor") @Cache(false) @@ -38,8 +34,6 @@ public CombiningArmorPlayerDisplayCreator(@NotNull Data data) { return new Display(data, zombiesPlayer); } - private static final TieredStack AIR = new TieredStack(ItemStack.AIR, -1); - public record TieredStack(@NotNull ItemStack stack, int tier) { } @@ -69,28 +63,21 @@ public void tick(long time) { return; } - if (ticks++ % 10 == 0) { - zombiesPlayer.getPlayer().ifPresent(player -> { - Map itemsToDisplay = new HashMap<>(4); - for (EquipmentSlot slot : EquipmentSlot.armors()) { - ItemStack existingStack = player.getEquipment(slot); - TieredStack ourStack = data.items.getOrDefault(slot, AIR); - - int existingTier = existingStack.getTag(Tags.ARMOR_TIER); - int ourTier = ourStack.tier; - - if (existingTier > ourTier) { - itemsToDisplay.put(slot, existingStack); - } - else { - itemsToDisplay.put(slot, ourStack.stack); - } - } - - for (Map.Entry entry : itemsToDisplay.entrySet()) { - armorStand.setEquipment(entry.getKey(), entry.getValue()); - } - }); + if (ticks++ % 10 != 0) { + return; + } + + Optional playerOptional = zombiesPlayer.getPlayer(); + if (playerOptional.isEmpty()) { + return; + } + + Player player = playerOptional.get(); + for (EquipmentSlot slot : EquipmentSlot.armors()) { + ItemStack existingStack = player.getEquipment(slot); + TieredStack ourStack = data.items.get(slot); + + armorStand.setEquipment(slot, ourStack == null ? existingStack : ourStack.stack); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/EquipmentUpgradeCostDisplayCreator.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/EquipmentUpgradeCostDisplayCreator.java index 97c46f24d..8239b8268 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/EquipmentUpgradeCostDisplayCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/display/creator/EquipmentUpgradeCostDisplayCreator.java @@ -5,6 +5,8 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.NotNull; @@ -95,8 +97,8 @@ private int applyModifiers(int cost) { private void updateCostDisplay() { Optional costOptional = computeCost(); if (costOptional.isPresent()) { - Component text = - MiniMessage.miniMessage().deserialize(String.format(data.formatString, costOptional.get())); + TagResolver costPlaceholder = Placeholder.component("cost", Component.text(costOptional.get())); + Component text = MiniMessage.miniMessage().deserialize(data.format, costPlaceholder); if (hologram.isEmpty()) { hologram.add(text); } @@ -130,7 +132,7 @@ public void tick(long time) { @DataObject public record Data(@NotNull Vec3D position, - @NotNull String formatString, + @NotNull String format, @NotNull Key equipmentKey, @NotNull Key groupKey, int baseCost, diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/ClickHandlerBase.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/ClickHandlerBase.java index c740161a0..f7b7e74ce 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/ClickHandlerBase.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/ClickHandlerBase.java @@ -3,21 +3,36 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.core.gui.Gui; import org.phantazm.core.gui.GuiItem; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; +import org.phantazm.zombies.map.shop.interactor.ShopInteractor; import org.phantazm.zombies.player.ZombiesPlayer; import java.util.Map; import java.util.Objects; import java.util.UUID; -public abstract class ClickHandlerBase implements GuiItem { +public abstract class ClickHandlerBase implements GuiItem, ShopInteractor { protected final TData data; protected final Map playerMap; + protected Shop shop; + public ClickHandlerBase(@NotNull TData data, @NotNull Map playerMap) { this.data = Objects.requireNonNull(data, "data"); this.playerMap = Objects.requireNonNull(playerMap, "playerMap"); } + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + return false; + } + @Override public void onRemove(@NotNull Gui owner, int slot) { @@ -27,4 +42,8 @@ public void onRemove(@NotNull Gui owner, int slot) { public void onReplace(@NotNull Gui owner, @NotNull GuiItem newItem, int slot) { } + + @Override + public void tick(long time) { + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/InteractingClickHandler.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/InteractingClickHandler.java index a313db3da..4369bf3ae 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/InteractingClickHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/gui/InteractingClickHandler.java @@ -1,6 +1,9 @@ package org.phantazm.zombies.map.shop.gui; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import net.minestom.server.entity.Player; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; @@ -9,6 +12,7 @@ import org.phantazm.core.item.UpdatingItem; import org.phantazm.zombies.map.BasicPlayerInteraction; import org.phantazm.zombies.map.shop.InteractionTypes; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.interactor.ShopInteractor; import org.phantazm.zombies.player.ZombiesPlayer; @@ -40,17 +44,30 @@ public InteractingClickHandler(@NotNull Data data, @NotNull Map clickTypes, boolean blacklist, @NotNull @ChildPath("updating_item") String updatingItem, - @NotNull @ChildPath("click_interactor") String clickInteractor) { + @NotNull @ChildPath("click_interactor") String clickInteractor, + boolean closeOnInteract) { + @Default("closeOnInteract") + public static @NotNull ConfigElement defaultCloseOnInteract() { + return ConfigPrimitive.of(true); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AddEquipmentInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AddEquipmentInteractor.java index 627a84b49..d2eb71c13 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AddEquipmentInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AddEquipmentInteractor.java @@ -1,23 +1,15 @@ package org.phantazm.zombies.map.shop.interactor; import com.github.steanky.element.core.annotation.*; -import com.github.steanky.toolkit.collection.Wrapper; import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.equipment.Equipment; -import org.phantazm.core.equipment.EquipmentHandler; -import org.phantazm.core.inventory.InventoryAccessRegistry; -import org.phantazm.core.inventory.InventoryObject; -import org.phantazm.core.inventory.InventoryObjectGroup; -import org.phantazm.core.inventory.InventoryProfile; import org.phantazm.zombies.map.shop.PlayerInteraction; -import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.map.shop.Shop; import java.util.List; -import java.util.Optional; @Model("zombies.map.shop.interactor.add_equipment") -@Cache +@Cache(false) public class AddEquipmentInteractor extends InteractorBase { private final List successInteractors; private final List failureInteractors; @@ -32,82 +24,30 @@ public AddEquipmentInteractor(@NotNull Data data, } @Override - public boolean handleInteraction(@NotNull PlayerInteraction interaction) { - return addEquipment(interaction); + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(successInteractors, shop); + ShopInteractor.initialize(failureInteractors, shop); } - private boolean addEquipment(PlayerInteraction interaction) { - ZombiesPlayer zombiesPlayer = interaction.player(); - EquipmentHandler handler = zombiesPlayer.module().getEquipmentHandler(); - - Wrapper wrapper = Wrapper.of(false); - if (data.allowDuplicate || !handler.hasEquipment(data.groupKey, data.equipmentKey)) { - if (handler.canAddEquipment(data.groupKey)) { - createEquipment(zombiesPlayer).ifPresent(equipment -> { - if (data.specificSlot < 0) { - handler.addEquipment(equipment, data.groupKey); - wrapper.set(true); - } - else { - InventoryAccessRegistry accessRegistry = handler.accessRegistry(); - - accessRegistry.getCurrentAccess().ifPresent(access -> { - InventoryObjectGroup group = access.groups().get(data.groupKey); - if (group != null && group.getSlots().contains(data.specificSlot)) { - InventoryProfile profile = group.getProfile(); - - if (profile.hasInventoryObject(data.specificSlot)) { - InventoryObject currentObject = profile.getInventoryObject(data.specificSlot); - if (data.allowReplace || group.defaultObject() == currentObject) { - accessRegistry.replaceObject(data.specificSlot, equipment); - wrapper.set(true); - } - } - else { - accessRegistry.replaceObject(data.specificSlot, equipment); - wrapper.set(true); - } - } - }); - } - }); - } - else if (data.allowReplace) { - zombiesPlayer.getPlayer().ifPresent(player -> { - int targetSlot = data.specificSlot < 0 ? player.getHeldSlot() : data.specificSlot; - - InventoryAccessRegistry accessRegistry = handler.accessRegistry(); - accessRegistry.getCurrentAccess().ifPresent(inventoryAccess -> { - InventoryObjectGroup targetGroup = inventoryAccess.groups().get(data.groupKey); - if (targetGroup != null && targetGroup.getSlots().contains(targetSlot)) { - createEquipment(zombiesPlayer).ifPresent(equipment -> { - wrapper.set(true); - accessRegistry.replaceObject(targetSlot, equipment); - }); - } - }); - }); - } - } - - boolean success = wrapper.get(); - if (success) { - for (ShopInteractor interactor : successInteractors) { - success &= interactor.handleInteraction(interaction); - } - - return success; - } - - for (ShopInteractor interactor : failureInteractors) { - interactor.handleInteraction(interaction); - } + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + boolean result = addOrReplaceEquipment(interaction); + List interactors = result ? successInteractors : failureInteractors; + return result & ShopInteractor.handle(interactors, interaction); + } - return false; + private boolean addOrReplaceEquipment(PlayerInteraction interaction) { + return interaction.player().module().getEquipmentHandler() + .addOrReplaceEquipment(data.groupKey, data.equipmentKey, data.allowReplace, data.specificSlot, + data.allowDuplicate, + () -> interaction.player().module().getEquipmentCreator().createEquipment(data.equipmentKey), + interaction.player().getPlayer().orElse(null)).success(); } - private Optional createEquipment(ZombiesPlayer zombiesPlayer) { - return zombiesPlayer.module().getEquipmentCreator().createEquipment(data.equipmentKey); + @Override + public void tick(long time) { + ShopInteractor.tick(successInteractors, time); + ShopInteractor.tick(failureInteractors, time); } @DataObject diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AdditiveTransactionModifierInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AdditiveTransactionModifierInteractor.java new file mode 100644 index 000000000..9937346da --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AdditiveTransactionModifierInteractor.java @@ -0,0 +1,78 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.coin.Transaction; +import org.phantazm.zombies.coin.TransactionModifierSource; +import org.phantazm.zombies.map.shop.PlayerInteraction; + +@Model("zombies.map.shop.interactor.additive_transaction_modifier") +@Cache(false) +public class AdditiveTransactionModifierInteractor implements ShopInteractor { + private final Data data; + private final TransactionModifierSource modifierSource; + + @FactoryMethod + public AdditiveTransactionModifierInteractor(@NotNull Data data, + @NotNull TransactionModifierSource modifierSource) { + this.data = data; + this.modifierSource = modifierSource; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + modifierSource.addModifier(data.modifierGroup, new Transaction.Modifier() { + @Override + public @NotNull Component getDisplayName() { + return data.displayName; + } + + @Override + public int modify(int coins) { + return switch (data.modifierAction) { + case ADD -> (int)Math.rint(coins + data.amount); + case ABS_ADD -> (int)Math.rint(coins + (coins < 0 ? -data.amount : data.amount)); + case MULTIPLY -> (int)Math.rint(coins + (data.amount * coins)); + }; + } + + @Override + public int getPriority() { + return data.priority; + } + }); + + return true; + } + + public enum ModifierAction { + ADD, + ABS_ADD, + MULTIPLY + } + + @DataObject + public record Data(@NotNull Key modifierGroup, + @NotNull ModifierAction modifierAction, + @NotNull Component displayName, + double amount, + int priority) { + @Default("displayName") + public static @NotNull ConfigElement defaultDisplayName() { + return ConfigPrimitive.of(""); + } + + @Default("priority") + public static @NotNull ConfigElement defaultPriority() { + return ConfigPrimitive.of(0); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AnnounceActiveSelectionGroup.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AnnounceActiveSelectionGroup.java new file mode 100644 index 000000000..100431bd5 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/AnnounceActiveSelectionGroup.java @@ -0,0 +1,80 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.Room; +import org.phantazm.zombies.map.objects.MapObjects; +import org.phantazm.zombies.map.shop.InteractorGroupHandler; +import org.phantazm.zombies.map.shop.PlayerInteraction; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +@Model("zombies.map.shop.interactor.announce_active_selection_group") +@Cache(false) +public class AnnounceActiveSelectionGroup implements ShopInteractor { + private final Data data; + private final Supplier mapObjects; + private final InteractorGroupHandler groupHandler; + + @FactoryMethod + public AnnounceActiveSelectionGroup(@NotNull Data data, @NotNull Supplier mapObjects, + @NotNull InteractorGroupHandler groupHandler) { + this.data = data; + this.mapObjects = mapObjects; + this.groupHandler = groupHandler; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + List interactors = groupHandler.interactors(data.group); + + SelectionGroupInteractor active = null; + for (SelectionGroupInteractor interactor : interactors) { + if (interactor.isActive()) { + active = interactor; + break; + } + } + + if (active == null) { + return false; + } + + Optional roomOptional = mapObjects.get().roomTracker().atPoint(active.shop().center()); + if (roomOptional.isEmpty()) { + return false; + } + + TagResolver tag = Placeholder.component("active_room", roomOptional.get().getRoomInfo().displayName()); + Component message = MiniMessage.miniMessage().deserialize(data.format, tag); + if (data.broadcast) { + mapObjects.get().module().instance().sendMessage(message); + } + else { + interaction.player().getPlayer().ifPresent(player -> player.sendMessage(message)); + } + + return true; + } + + @DataObject + public record Data(@NotNull Key group, @NotNull String format, boolean broadcast) { + @Default("broadcast") + public static @NotNull ConfigElement defaultBroadcast() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChangeSelectionGroupInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChangeSelectionGroupInteractor.java new file mode 100644 index 000000000..08ba4596a --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChangeSelectionGroupInteractor.java @@ -0,0 +1,151 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.zombies.map.objects.MapObjects; +import org.phantazm.zombies.map.shop.InteractorGroupHandler; +import org.phantazm.zombies.map.shop.PlayerInteraction; + +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.function.Supplier; + +@Model("zombies.map.shop.interactor.change_selection_group") +@Cache(false) +public class ChangeSelectionGroupInteractor implements ShopInteractor { + private static final Component UNKNOWN_ROOM = Component.text("an unknown room"); + + private final Data data; + private final InteractorGroupHandler groupHandler; + private final Random random; + private final Supplier mapObjects; + + @FactoryMethod + public ChangeSelectionGroupInteractor(@NotNull Data data, @NotNull InteractorGroupHandler groupHandler, + @NotNull Random random, @NotNull Supplier mapObjects) { + this.data = data; + this.groupHandler = groupHandler; + this.random = random; + this.mapObjects = mapObjects; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + List interactors = groupHandler.interactors(data.group); + if (interactors.isEmpty()) { + return false; + } + + boolean hasCurrentlyActiveInteractor = false; + int currentActiveInteractorIndex = 0; + for (SelectionGroupInteractor interactor : interactors) { + if (interactor.isActive()) { + hasCurrentlyActiveInteractor = true; + break; + } + + currentActiveInteractorIndex++; + } + + if (!hasCurrentlyActiveInteractor) { + SelectionGroupInteractor newActive = interactors.get(random.nextInt(interactors.size())); + newActive.setActive(true); + maybeSendMessage(newActive, interaction); + return true; + } + + if (data.excludeCurrent) { + if (interactors.size() == 1) { + return true; + } + + int newIndex = random.nextInt(interactors.size() - 1); + if (newIndex >= currentActiveInteractorIndex) { + newIndex++; + } + + interactors.get(currentActiveInteractorIndex).setActive(false); + + SelectionGroupInteractor newActive = interactors.get(newIndex); + newActive.setActive(true); + maybeSendMessage(newActive, interaction); + return true; + } + + SelectionGroupInteractor newInteractor = interactors.get(random.nextInt(interactors.size())); + SelectionGroupInteractor oldInteractor = interactors.get(currentActiveInteractorIndex); + if (newInteractor == oldInteractor) { + return true; + } + + oldInteractor.setActive(false); + newInteractor.setActive(true); + maybeSendMessage(newInteractor, interaction); + return true; + } + + private void maybeSendMessage(SelectionGroupInteractor newActive, PlayerInteraction interaction) { + if (data.moveMessage == null) { + return; + } + + Optional playerOptional = interaction.player().getPlayer(); + if (playerOptional.isEmpty()) { + return; + } + + Player player = playerOptional.get(); + + MapObjects mapObjects = this.mapObjects.get(); + Component roomName = mapObjects.roomTracker().atPoint(newActive.shop().center()) + .map(room -> room.getRoomInfo().displayName()).orElse(UNKNOWN_ROOM); + + TagResolver roomNameTag = Placeholder.component("room_name", roomName); + Component message = MiniMessage.miniMessage().deserialize(data.moveMessage, roomNameTag); + + if (data.broadcast) { + Instance instance = player.getInstance(); + if (instance == null) { + return; + } + + instance.sendMessage(message); + } + else { + player.sendMessage(message); + } + } + + @DataObject + public record Data(@NotNull Key group, boolean excludeCurrent, @Nullable String moveMessage, boolean broadcast) { + @Default("excludeCurrent") + public static @NotNull ConfigElement defaultExcludeCurrent() { + return ConfigPrimitive.of(true); + } + + @Default("moveMessage") + public static @NotNull ConfigElement defaultMoveMessage() { + return ConfigPrimitive.NULL; + } + + @Default("broadcast") + public static @NotNull ConfigElement defaultBroadcast() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChestStateInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChestStateInteractor.java new file mode 100644 index 000000000..b95b1b83e --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ChestStateInteractor.java @@ -0,0 +1,51 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.packet.server.play.BlockActionPacket; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +@Model("zombies.map.shop.interactor.chest_state") +@Cache(false) +public class ChestStateInteractor implements ShopInteractor { + private final Data data; + + private Shop shop; + + @FactoryMethod + public ChestStateInteractor(@NotNull Data data) { + this.data = data; + } + + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + Chunk chunk = shop.instance().getChunkAt(shop.center()); + if (chunk == null) { + return false; + } + + if (data.open) { + chunk.sendPacketToViewers(new BlockActionPacket(shop.center(), (byte)1, (byte)1, Block.CHEST)); + } + else { + chunk.sendPacketToViewers(new BlockActionPacket(shop.center(), (byte)1, (byte)0, Block.CHEST)); + } + + return true; + } + + @DataObject + public record Data(boolean open) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ConditionalInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ConditionalInteractor.java index ce2b65487..7f1457e7b 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ConditionalInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ConditionalInteractor.java @@ -17,6 +17,8 @@ public class ConditionalInteractor extends InteractorBase successInteractors; private final List failureInteractors; + private Shop shop; + @FactoryMethod public ConditionalInteractor(@NotNull Data data, @Child("predicates") List predicates, @Child("success_interactors") List successInteractors, @@ -29,36 +31,22 @@ public ConditionalInteractor(@NotNull Data data, @Child("predicates") List interactors = success ? successInteractors : failureInteractors; - - for (ShopInteractor interactor : interactors) { - success &= interactor.handleInteraction(interaction); - } - - return success; + return success & ShopInteractor.handle(interactors, interaction); } @Override public void tick(long time) { - for (ShopInteractor interactor : successInteractors) { - interactor.tick(time); - } - - for (ShopInteractor interactor : failureInteractors) { - interactor.tick(time); - } + ShopInteractor.tick(successInteractors, time); + ShopInteractor.tick(failureInteractors, time); } @Override public void initialize(@NotNull Shop shop) { - for (ShopInteractor interactor : successInteractors) { - interactor.initialize(shop); - } - - for (ShopInteractor interactor : failureInteractors) { - interactor.initialize(shop); - } + this.shop = shop; + ShopInteractor.initialize(successInteractors, shop); + ShopInteractor.initialize(failureInteractors, shop); } @DataObject diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CostSubstitutingItem.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CostSubstitutingItem.java new file mode 100644 index 000000000..146834ea9 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CostSubstitutingItem.java @@ -0,0 +1,98 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.core.ItemStackUtils; +import org.phantazm.core.item.UpdatingItem; +import org.phantazm.zombies.coin.Transaction; +import org.phantazm.zombies.coin.TransactionModifierSource; + +import java.util.List; + +@Model("item.updating.cost_substituting") +@Cache(false) +public class CostSubstitutingItem implements UpdatingItem { + private static final int UPDATE_INTERVAL = 10; + + private final Data data; + private final TransactionModifierSource modifierSource; + + private ItemStack itemStack; + private int ticks; + + private boolean hasOldCost; + private int oldCost; + + @FactoryMethod + public CostSubstitutingItem(@NotNull Data data, @NotNull TransactionModifierSource modifierSource) { + this.data = data; + this.modifierSource = modifierSource; + this.itemStack = computeItemStack(data.cost); + } + + @Override + public @NotNull ItemStack update(long time, @NotNull ItemStack current) { + int cost = cost(); + this.oldCost = cost; + this.hasOldCost = true; + return itemStack = computeItemStack(cost); + } + + @Override + public boolean hasUpdate(long time, @NotNull ItemStack current) { + return (ticks++ % UPDATE_INTERVAL == 0 && (!hasOldCost || cost() != oldCost)); + } + + @Override + public @NotNull ItemStack currentItem() { + return itemStack; + } + + private ItemStack computeItemStack(int cost) { + return ItemStackUtils.buildItem(data.material, data.tag, data.displayName, data.lore, + Placeholder.unparsed("cost", Integer.toString(cost))); + } + + private int cost() { + int cost = data.cost; + for (Transaction.Modifier modifier : modifierSource.modifiers(data.modifier)) { + cost = modifier.modify(cost); + } + + return cost; + } + + @DataObject + public record Data(@NotNull Material material, + @Nullable String displayName, + @Nullable List lore, + @Nullable String tag, + int cost, + @NotNull Key modifier) { + @Default("displayName") + public static @NotNull ConfigElement defaultDisplayName() { + return ConfigPrimitive.NULL; + } + + @Default("lore") + public static @NotNull ConfigElement defaultLore() { + return ConfigPrimitive.NULL; + } + + @Default("tag") + public static @NotNull ConfigElement defaultTag() { + return ConfigPrimitive.NULL; + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CountingInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CountingInteractor.java new file mode 100644 index 000000000..65a106935 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/CountingInteractor.java @@ -0,0 +1,83 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.List; + +@Model("zombies.map.shop.interactor.counting") +@Cache(false) +public class CountingInteractor extends InteractorBase { + private final List successInteractors; + private final List failureInteractors; + + private int uses; + + @FactoryMethod + public CountingInteractor(@NotNull Data data, @NotNull @Child("success") List successInteractors, + @NotNull @Child("failure") List failureInteractors) { + super(data); + this.successInteractors = successInteractors; + this.failureInteractors = failureInteractors; + } + + @Override + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(successInteractors, shop); + ShopInteractor.initialize(failureInteractors, shop); + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + int uses = this.uses; + + if (uses < data.maxUses) { + ShopInteractor.handle(successInteractors, interaction); + } + + boolean success = true; + if (data.reset) { + int nextUse = ++uses % data.maxUses; + if (nextUse < uses) { + success = false; + ShopInteractor.handle(failureInteractors, interaction); + } + + this.uses = nextUse; + return success; + } + + int nextUse = ++uses; + if (nextUse >= data.maxUses) { + success = false; + ShopInteractor.handle(failureInteractors, interaction); + } + else { + this.uses = nextUse; + } + + return success; + } + + @Override + public void tick(long time) { + ShopInteractor.tick(successInteractors, time); + ShopInteractor.tick(failureInteractors, time); + } + + @DataObject + public record Data(int maxUses, + boolean reset, + @NotNull @ChildPath("success") List successInteractors, + @NotNull @ChildPath("failure") List failureInteractors) { + @Default("reset") + public static @NotNull ConfigElement resetDefault() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DelayedInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DelayedInteractor.java index e6554d45f..383be26f1 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DelayedInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DelayedInteractor.java @@ -4,50 +4,60 @@ import net.minestom.server.MinecraftServer; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; -import java.util.Objects; +import java.util.*; @Model("zombies.map.shop.interactor.delayed") @Cache(false) public class DelayedInteractor extends InteractorBase { - private final ShopInteractor target; + private final List interactors; - private PlayerInteraction interaction; - private long startTime; + private final Deque interactions; + + private record Interaction(long startTime, PlayerInteraction interaction) { + } @FactoryMethod - public DelayedInteractor(@NotNull Data data, @NotNull @Child("target") ShopInteractor target) { + public DelayedInteractor(@NotNull Data data, @NotNull @Child("target") List interactors) { super(data); - this.target = Objects.requireNonNull(target, "target"); + this.interactors = Objects.requireNonNull(interactors, "interactors"); + this.interactions = new ArrayDeque<>(); } @Override - public boolean handleInteraction(@NotNull PlayerInteraction interaction) { - if (this.interaction == null || data.resetOnInteract) { - this.startTime = System.currentTimeMillis(); - this.interaction = interaction; - } + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(interactors, shop); + } + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + interactions.add(new Interaction(System.currentTimeMillis(), interaction)); return true; } @Override public void tick(long time) { - super.tick(time); + long currentTime = System.currentTimeMillis(); - if (interaction != null) { - long elapsedTimeMs = time - startTime; - int elapsedTicks = (int)(elapsedTimeMs / MinecraftServer.TICK_MS); + Iterator interactionIterator = interactions.iterator(); + while (interactionIterator.hasNext()) { + Interaction interaction = interactionIterator.next(); - if (elapsedTicks >= data.delayTicks) { - target.handleInteraction(interaction); - interaction = null; + long elapsedTicks = (currentTime - interaction.startTime) / MinecraftServer.TICK_MS; + if (elapsedTicks < data.delayTicks) { + break; } + + ShopInteractor.handle(interactors, interaction.interaction); + interactionIterator.remove(); } + + ShopInteractor.tick(interactors, time); } @DataObject - record Data(@ChildPath("target") String targetPath, int delayTicks, boolean resetOnInteract) { + public record Data(@ChildPath("target") List interactors, int delayTicks) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DragonsWrathInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DragonsWrathInteractor.java new file mode 100644 index 000000000..0bf47a4fa --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/DragonsWrathInteractor.java @@ -0,0 +1,100 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.toolkit.collection.Wrapper; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.EntityTracker; +import net.minestom.server.instance.Instance; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.ExtraNodeKeys; +import org.phantazm.zombies.Tags; +import org.phantazm.zombies.map.Round; +import org.phantazm.zombies.map.handler.RoundHandler; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.Optional; +import java.util.function.Supplier; + +@Model("zombies.map.shop.interactor.dragons_wrath") +@Cache(false) +public class DragonsWrathInteractor implements ShopInteractor { + private final Data data; + private final Supplier roundHandler; + private final MobStore mobStore; + private Shop shop; + + @FactoryMethod + public DragonsWrathInteractor(@NotNull Data data, @NotNull Supplier roundHandler, + @NotNull MobStore mobStore) { + this.data = data; + this.roundHandler = roundHandler; + this.mobStore = mobStore; + } + + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + Instance instance = shop.instance(); + RoundHandler roundHandler = this.roundHandler.get(); + Optional currentRoundOptional = roundHandler.currentRound(); + if (currentRoundOptional.isEmpty()) { + return false; + } + + Round currentRound = currentRoundOptional.get(); + Wrapper killCount = Wrapper.of(0); + instance.getEntityTracker() + .nearbyEntities(shop.center(), data.radius, EntityTracker.Target.LIVING_ENTITIES, entity -> { + if (!currentRound.hasMob(entity.getUuid())) { + return; + } + + PhantazmMob mob = mobStore.getMob(entity.getUuid()); + if (mob == null || + mob.model().getExtraNode().getBooleanOrDefault(false, ExtraNodeKeys.RESIST_INSTAKILL)) { + return; + } + + Entity lightningBolt = new Entity(EntityType.LIGHTNING_BOLT); + lightningBolt.setInstance(instance, entity.getPosition()); + lightningBolt.scheduleRemove(20, TimeUnit.SERVER_TICK); + + instance.playSound(data.sound()); + + entity.setTag(Tags.LAST_HIT_BY, interaction.player().module().getPlayerView().getUUID()); + entity.kill(); + killCount.set(killCount.get() + 1); + }); + + TagResolver killCountPlaceholder = Placeholder.component("kill_count", Component.text(killCount.get())); + Component message = MiniMessage.miniMessage().deserialize(data.messageFormat(), killCountPlaceholder); + if (data.broadcast()) { + instance.sendMessage(message); + } else { + interaction.player().getPlayer().ifPresent(player -> player.sendMessage(message)); + } + + return true; + } + + @DataObject + public record Data(@NotNull Sound sound, @NotNull String messageFormat, boolean broadcast, double radius) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/EquipmentStationInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/EquipmentStationInteractor.java new file mode 100644 index 000000000..29889b31c --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/EquipmentStationInteractor.java @@ -0,0 +1,117 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.equipment.Equipment; +import org.phantazm.core.equipment.EquipmentHandler; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.List; +import java.util.Optional; + +@Model("zombies.map.shop.interactor.equipment_station") +@Cache(false) +public class EquipmentStationInteractor implements ShopInteractor { + private final Data data; + private final List successInteractors; + private final List holdingInteractors; + private final List duplicateInteractors; + private final List failureInteractors; + + @FactoryMethod + public EquipmentStationInteractor(@NotNull Data data, + @NotNull @Child("success") List successInteractors, + @NotNull @Child("holding") List holdingInteractors, + @NotNull @Child("duplicate") List duplicateInteractors, + @NotNull @Child("failure") List failureInteractors) { + this.data = data; + this.successInteractors = successInteractors; + this.holdingInteractors = holdingInteractors; + this.duplicateInteractors = duplicateInteractors; + this.failureInteractors = failureInteractors; + } + + @Override + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(successInteractors, shop); + ShopInteractor.initialize(holdingInteractors, shop); + ShopInteractor.initialize(duplicateInteractors, shop); + ShopInteractor.initialize(failureInteractors, shop); + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + Optional playerOptional = interaction.player().getPlayer(); + if (playerOptional.isEmpty()) { + return false; + } + + EquipmentHandler.Result result = interaction.player().module().getEquipmentHandler() + .addOrReplaceEquipment(data.groupKey, data.equipmentKey, data.allowReplace, data.specificSlot, + data.allowDuplicate, + () -> interaction.player().module().getEquipmentCreator().createEquipment(data.equipmentKey), + playerOptional.get()); + + if (!result.success()) { + Optional equipmentOptional = interaction.player().getHeldEquipment(); + if (equipmentOptional.isPresent()) { + Equipment equipment = equipmentOptional.get(); + if (equipment.key().equals(data.equipmentKey)) { + return ShopInteractor.handle(holdingInteractors, interaction); + } + } + } + + return switch (result) { + case ADDED, REPLACED -> ShopInteractor.handle(successInteractors, interaction); + case DUPLICATE -> { + ShopInteractor.handle(duplicateInteractors, interaction); + yield false; + } + case FAILED -> { + ShopInteractor.handle(failureInteractors, interaction); + yield false; + } + }; + } + + @Override + public void tick(long time) { + ShopInteractor.tick(successInteractors, time); + ShopInteractor.tick(holdingInteractors, time); + ShopInteractor.tick(duplicateInteractors, time); + ShopInteractor.tick(failureInteractors, time); + } + + @DataObject + public record Data(@NotNull Key equipmentKey, + @NotNull Key groupKey, + boolean allowReplace, + int specificSlot, + boolean allowDuplicate, + @NotNull @ChildPath("success") List successInteractors, + @NotNull @ChildPath("holding") List holdingInteractors, + @NotNull @ChildPath("duplicate") List duplicateInteractors, + @NotNull @ChildPath("failure") List failureInteractors) { + @Default("allowReplace") + public static ConfigElement defaultAllowReplace() { + return ConfigPrimitive.of(true); + } + + @Default("specificSlot") + public static ConfigElement defaultSpecificSlot() { + return ConfigPrimitive.of(-1); + } + + @Default("allowDuplicate") + public static ConfigElement defaultAllowDuplicate() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MapFlaggingInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MapFlaggingInteractor.java index 65560c3d1..18cce2bd1 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MapFlaggingInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MapFlaggingInteractor.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.shop.interactor; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -12,6 +13,7 @@ import java.util.Objects; @Model("zombies.map.shop.interactor.map_flagging") +@Cache(false) public class MapFlaggingInteractor extends InteractorBase { private final Flaggable flaggable; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MessagingInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MessagingInteractor.java index 5f709708e..7962a91d6 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MessagingInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/MessagingInteractor.java @@ -1,10 +1,15 @@ package org.phantazm.zombies.map.shop.interactor; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; @@ -13,6 +18,7 @@ import java.util.Objects; @Model("zombies.map.shop.interactor.messaging") +@Cache(false) public class MessagingInteractor extends InteractorBase { private final Instance instance; @@ -25,22 +31,29 @@ public MessagingInteractor(@NotNull Data data, @NotNull Instance instance) { @Override public boolean handleInteraction(@NotNull PlayerInteraction interaction) { if (data.broadcast) { - sendMessages(instance); + interaction.player().module().getPlayerView().getPlayer() + .ifPresent(player -> sendMessages(instance, player)); + } else { - interaction.player().module().getPlayerView().getPlayer().ifPresent(this::sendMessages); + interaction.player().module().getPlayerView().getPlayer().ifPresent(player -> sendMessages(player, player)); } return true; } - private void sendMessages(Audience audience) { - for (Component component : data.messages) { - audience.sendMessage(component); + private void sendMessages(Audience audience, Player player) { + Component displayName = player.getDisplayName(); + if (displayName == null) { + displayName = player.getName(); + } + TagResolver sender = Placeholder.component("sender", displayName); + for (String format : data.messages) { + audience.sendMessage(MiniMessage.miniMessage().deserialize(format, sender)); } } @DataObject - public record Data(@NotNull List messages, boolean broadcast) { + public record Data(@NotNull List messages, boolean broadcast) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/OpenGuiInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/OpenGuiInteractor.java index d5012843a..ed43df8bb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/OpenGuiInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/OpenGuiInteractor.java @@ -5,39 +5,50 @@ import net.minestom.server.inventory.InventoryType; import org.jetbrains.annotations.NotNull; import org.phantazm.core.gui.Gui; -import org.phantazm.core.gui.GuiItem; import org.phantazm.core.gui.SlotDistributor; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; +import org.phantazm.zombies.map.shop.gui.ClickHandlerBase; import java.util.List; import java.util.Objects; @Model("zombies.map.shop.interactor.open_gui") +@Cache(false) public class OpenGuiInteractor extends InteractorBase { private final SlotDistributor slotDistributor; - private final List guiItems; + private final List> guiItems; @FactoryMethod public OpenGuiInteractor(@NotNull Data data, @NotNull SlotDistributor slotDistributor, - @NotNull @Child("items") List guiItems) { + @NotNull @Child("items") List> guiItems) { super(data); this.slotDistributor = Objects.requireNonNull(slotDistributor, "slotDistributor"); this.guiItems = Objects.requireNonNull(guiItems, "guiItems"); } + @Override + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(guiItems, shop); + } + @Override public boolean handleInteraction(@NotNull PlayerInteraction interaction) { interaction.player().module().getPlayerView().getPlayer().ifPresent(player -> player.openInventory( - Gui.builder(data.inventoryType, slotDistributor).setDynamic(data.dynamic).withItems(guiItems) + Gui.builder(data.inventoryType, slotDistributor).setDynamic(false).withItems(guiItems) .withTitle(data.title).build())); return true; } + @Override + public void tick(long time) { + ShopInteractor.tick(guiItems, time); + } + @DataObject public record Data(@NotNull Component title, @NotNull InventoryType inventoryType, - @NotNull @ChildPath("items") List guiItems, - boolean dynamic) { + @NotNull @ChildPath("items") List guiItems) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySongInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySongInteractor.java new file mode 100644 index 000000000..94fb6f0bb --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySongInteractor.java @@ -0,0 +1,105 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.sound.SongLoader; +import org.phantazm.core.sound.SongPlayer; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.List; +import java.util.Optional; + +@Model("zombies.map.shop.interactor.play_song") +@Cache(false) +public class PlaySongInteractor implements ShopInteractor { + private final Data data; + private final SongLoader songLoader; + private final SongPlayer songPlayer; + + private Shop shop; + + @FactoryMethod + public PlaySongInteractor(@NotNull Data data, @NotNull SongLoader songLoader, @NotNull SongPlayer songPlayer) { + this.data = data; + this.songLoader = songLoader; + this.songPlayer = songPlayer; + } + + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + if (!songLoader.songs().contains(data.songKey)) { + return false; + } + + List notes = songLoader.getNotes(data.songKey); + Optional playerOptional = interaction.player().getPlayer(); + if (data.broadcast) { + playerOptional.ifPresent(player -> { + Instance instance = player.getInstance(); + if (instance != null) { + if (data.atLocation) { + songPlayer.play(instance, data.source, shop.center(), notes, data.volume); + } + else { + songPlayer.play(instance, data.source, Sound.Emitter.self(), notes, data.volume); + } + } + }); + + return true; + } + + if (data.atLocation) { + playerOptional.ifPresent(player -> songPlayer.play(player, data.source, shop.center(), notes, data.volume)); + } + else { + playerOptional.ifPresent( + player -> songPlayer.play(player, data.source, Sound.Emitter.self(), notes, data.volume)); + } + + return true; + } + + @DataObject + public record Data(@NotNull Key songKey, + boolean broadcast, + boolean atLocation, + float volume, + @NotNull Sound.Source source) { + @Default("broadcast") + public static @NotNull ConfigElement broadcastDefault() { + return ConfigPrimitive.of(false); + } + + @Default("atLocation") + public static @NotNull ConfigElement atLocationDefault() { + return ConfigPrimitive.of(true); + } + + @Default("volume") + public static @NotNull ConfigElement volumeDefault() { + return ConfigPrimitive.of(2F); + } + + @Default("source") + public static @NotNull ConfigElement sourceDefault() { + return ConfigPrimitive.of("MASTER"); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySoundInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySoundInteractor.java index 3a924b407..99472f0b4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySoundInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/PlaySoundInteractor.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.shop.interactor; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -15,6 +16,7 @@ Plays a Minecraft sound. Can either be specific to the player activating this shop, or global (instance-wide). """) @Model("zombies.map.shop.interactor.play_sound") +@Cache(false) public class PlaySoundInteractor extends InteractorBase { private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/RefillAllAmmoInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/RefillAllAmmoInteractor.java new file mode 100644 index 000000000..297188bac --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/RefillAllAmmoInteractor.java @@ -0,0 +1,46 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.inventory.InventoryAccess; +import org.phantazm.core.inventory.InventoryObject; +import org.phantazm.zombies.equipment.gun.Gun; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.player.state.InventoryKeys; + +import java.util.Map; +import java.util.UUID; + +@Model("zombies.map.shop.interactor.refill_all_ammo") +@Cache(false) +public class RefillAllAmmoInteractor implements ShopInteractor { + private final Map playerMap; + + @FactoryMethod + public RefillAllAmmoInteractor(@NotNull Map playerMap) { + this.playerMap = playerMap; + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + for (ZombiesPlayer zombiesPlayer : playerMap.values()) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + InventoryAccess access = + zombiesPlayer.module().getInventoryAccessRegistry().getAccess(InventoryKeys.ALIVE_ACCESS); + + for (InventoryObject object : access.profile().objects()) { + if (object instanceof Gun gun) { + gun.refill(); + } + } + } + + return true; + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ReviveAllInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ReviveAllInteractor.java new file mode 100644 index 000000000..7dfd833f8 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ReviveAllInteractor.java @@ -0,0 +1,100 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; +import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.player.state.ZombiesPlayerState; +import org.phantazm.zombies.player.state.ZombiesPlayerStateKeys; +import org.phantazm.zombies.player.state.context.AlivePlayerStateContext; +import org.phantazm.zombies.player.state.revive.KnockedPlayerState; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@Model("zombies.map.shop.interactor.revive_all") +@Cache(false) +public class ReviveAllInteractor implements ShopInteractor { + private final Data data; + private final Map playerMap; + private final Pos respawnPos; + private Instance instance; + + @FactoryMethod + public ReviveAllInteractor(@NotNull Data data, @NotNull Map playerMap, + @NotNull Pos respawnPos) { + this.data = Objects.requireNonNull(data, "data"); + this.playerMap = playerMap; + this.respawnPos = respawnPos; + } + + @Override + public void initialize(@NotNull Shop shop) { + this.instance = shop.instance(); + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + Optional playerOptional = interaction.player().getPlayer(); + if (playerOptional.isEmpty()) { + return false; + } + + Player reviver = playerOptional.get(); + Component reviveName = reviver.getDisplayName(); + int reviveCount = 0; + for (ZombiesPlayer zombiesPlayer : playerMap.values()) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + ZombiesPlayerState state = zombiesPlayer.module().getStateSwitcher().getState(); + Point reviveLocation = null; + + if (state instanceof KnockedPlayerState knocked) { + reviveLocation = knocked.getReviveHandler().context().getKnockLocation(); + } + + boolean dead = zombiesPlayer.isDead(); + if (dead || zombiesPlayer.isKnocked()) { + if (dead) { + zombiesPlayer.getPlayer().ifPresent(player -> player.teleport(respawnPos)); + } + + zombiesPlayer.setState(ZombiesPlayerStateKeys.ALIVE, + AlivePlayerStateContext.revive(reviveName, reviveLocation)); + ++reviveCount; + } + } + + TagResolver reviveCountPlaceholder = Placeholder.component("revive_count", Component.text(reviveCount)); + Component message = MiniMessage.miniMessage().deserialize(data.messageFormat(), reviveCountPlaceholder); + if (data.broadcast()) { + instance.sendMessage(message); + } else { + reviver.sendMessage(message); + } + + return true; + } + + @DataObject + public record Data(String messageFormat, boolean broadcast) { + + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SelectionGroupInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SelectionGroupInteractor.java new file mode 100644 index 000000000..0751d4c90 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SelectionGroupInteractor.java @@ -0,0 +1,126 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.*; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.shop.InteractorGroupHandler; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +@Model("zombies.map.shop.interactor.selection_group") +@Cache(false) +public class SelectionGroupInteractor implements ShopInteractor { + private final Data data; + private final List inactiveInteractors; + private final List activeInteractors; + + private final InteractorGroupHandler handler; + private final Random random; + private final List> initializationCallbacks; + + private boolean active; + private Shop shop; + + @FactoryMethod + public SelectionGroupInteractor(@NotNull Data data, + @NotNull @Child("active_interactors") List activeInteractors, + @NotNull @Child("inactive_interactors") List inactiveInteractors, + @NotNull InteractorGroupHandler handler, @NotNull Random random) { + this.data = data; + this.activeInteractors = activeInteractors; + this.inactiveInteractors = inactiveInteractors; + this.handler = handler; + this.random = random; + this.initializationCallbacks = new ArrayList<>(); + + handler.subscribe(data.group, this); + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + if (!active) { + ShopInteractor.handle(inactiveInteractors, interaction); + return false; + } + + return ShopInteractor.handle(activeInteractors, interaction); + } + + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + + ShopInteractor.initialize(activeInteractors, shop); + ShopInteractor.initialize(inactiveInteractors, shop); + + for (Consumer consumer : initializationCallbacks) { + consumer.accept(this); + } + + initializationCallbacks.clear(); + + if (handler.isChosen(data.group)) { + return; + } + + List interactors = handler.interactors(data.group); + handler.markChosen(data.group); + + if (interactors.isEmpty()) { + return; + } + + interactors.get(random.nextInt(interactors.size())) + .addInitializationCallback(interactor -> interactor.setActive(true)); + } + + @Override + public void tick(long time) { + ShopInteractor.tick(activeInteractors, time); + ShopInteractor.tick(inactiveInteractors, time); + } + + public Shop shop() { + return shop; + } + + private void addInitializationCallback(@NotNull Consumer consumer) { + if (this.shop == null) { + initializationCallbacks.add(consumer); + } + else { + consumer.accept(this); + } + } + + public void setActive(boolean active) { + boolean currentlyActive = this.active; + if (currentlyActive == active) { + return; + } + + if (active) { + shop.flags().setFlag(data.group); + } + else { + shop.flags().clearFlag(data.group); + } + + this.active = active; + } + + public boolean isActive() { + return this.active; + } + + @DataObject + public record Data(@NotNull Key group, + @NotNull @ChildPath("active_interactors") List activeInteractors, + @NotNull @ChildPath("inactive_interactors") List inactiveInteractors) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ShopInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ShopInteractor.java index 6e69e196c..1bfda1d58 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ShopInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/ShopInteractor.java @@ -15,4 +15,26 @@ default void tick(long time) { default void initialize(@NotNull Shop shop) { } + + static boolean handle(@NotNull Iterable interactors, + @NotNull PlayerInteraction interaction) { + boolean res = true; + for (ShopInteractor interactor : interactors) { + res &= interactor.handleInteraction(interaction); + } + + return res; + } + + static void tick(@NotNull Iterable interactors, long time) { + for (ShopInteractor interactor : interactors) { + interactor.tick(time); + } + } + + static void initialize(@NotNull Iterable interactors, @NotNull Shop shop) { + for (ShopInteractor interactor : interactors) { + interactor.initialize(shop); + } + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SlotMachineInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SlotMachineInteractor.java new file mode 100644 index 000000000..8a3699b1d --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SlotMachineInteractor.java @@ -0,0 +1,431 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.*; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.metadata.item.ItemEntityMeta; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.core.hologram.Hologram; +import org.phantazm.core.hologram.InstanceHologram; +import org.phantazm.core.time.TickFormatter; +import org.phantazm.zombies.map.Evaluation; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; +import org.phantazm.zombies.map.shop.predicate.ShopPredicate; + +import java.util.*; + +@Model("zombies.map.shop.interactor.slot_machine") +@Cache(false) +public class SlotMachineInteractor implements ShopInteractor { + private final Data data; + private final List rollPredicates; + private final TickFormatter tickFormatter; + private final DelayFormula delayFormula; + private final List frames; + private final List rollFailInteractors; + private final List rollStartInteractors; + private final List mismatchedPlayerInteractors; + private final List whileRollingInteractors; + private final List timeoutExpiredInteractors; + private final List itemClaimedInteractors; + private final List endInteractors; + private final Random random; + + //non-null in tick and interaction methods - set in initialize(Shop) + private Shop shop; + private Hologram hologram; + + private Entity item; + + private PlayerInteraction rollInteraction; + private boolean rolling; + private boolean doneRolling; + + private long rollFinishTime; + + private long lastFrameTime; + private int currentFrameIndex; + private int ticksUntilNextFrame; + + @FactoryMethod + public SlotMachineInteractor(Data data, @NotNull @Child("roll_predicates") List rollPredicates, + @NotNull @Child("tick_formatter") TickFormatter tickFormatter, + @NotNull @Child("delay_formula") DelayFormula delayFormula, + @NotNull @Child("frames") List frames, + @NotNull @Child("roll_fail_interactors") List rollFailInteractors, + @NotNull @Child("roll_start_interactors") List rollStartInteractors, + @NotNull @Child("mismatched_player_interactors") List mismatchedPlayerInteractors, + @NotNull @Child("while_rolling_interactors") List whileRollingInteractors, + @NotNull @Child("timeout_expired_interactors") List timeoutExpiredInteractors, + @NotNull @Child("item_claimed_interactors") List itemClaimedInteractors, + @NotNull @Child("end_interactors") List endInteractors, @NotNull Random random) { + this.data = data; + this.rollPredicates = rollPredicates; + this.tickFormatter = tickFormatter; + this.delayFormula = delayFormula; + this.frames = new ArrayList<>(frames); + this.rollFailInteractors = rollFailInteractors; + this.rollStartInteractors = rollStartInteractors; + this.mismatchedPlayerInteractors = mismatchedPlayerInteractors; + this.whileRollingInteractors = whileRollingInteractors; + this.timeoutExpiredInteractors = timeoutExpiredInteractors; + this.itemClaimedInteractors = itemClaimedInteractors; + this.endInteractors = endInteractors; + this.random = random; + } + + @Override + public void initialize(@NotNull Shop shop) { + this.shop = shop; + this.hologram = new InstanceHologram(shop.center().add(0, data.hologramOffset, 0), 0); + this.hologram.setInstance(shop.instance()); + + ShopInteractor.initialize(rollStartInteractors, shop); + ShopInteractor.initialize(mismatchedPlayerInteractors, shop); + ShopInteractor.initialize(whileRollingInteractors, shop); + ShopInteractor.initialize(timeoutExpiredInteractors, shop); + ShopInteractor.initialize(itemClaimedInteractors, shop); + ShopInteractor.initialize(endInteractors, shop); + + for (SlotMachineFrame frame : frames) { + ShopInteractor.initialize(frame.interactors(), shop); + } + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + if (rolling) { + if (interaction.player() != rollInteraction.player()) { + ShopInteractor.handle(mismatchedPlayerInteractors, interaction); + return false; + } + + if (!doneRolling) { + ShopInteractor.handle(whileRollingInteractors, interaction); + return false; + } + + if (!frames.isEmpty()) { + SlotMachineFrame currentFrame = frames.get((currentFrameIndex - 1) % frames.size()); + if (!ShopInteractor.handle(currentFrame.interactors(), rollInteraction)) { + return false; + } + + rollInteraction.player().getPlayer().ifPresent(player -> player.sendMessage(MiniMessage.miniMessage() + .deserialize(data.itemClaimedFormat, + Placeholder.component("claimed_item", getItemName(currentFrame.getVisual()))))); + } + + ShopInteractor.handle(itemClaimedInteractors, rollInteraction); + reset(); + return true; + } + + if (!data.evaluation.evaluate(rollPredicates, interaction, shop)) { + ShopInteractor.handle(rollFailInteractors, interaction); + return false; + } + + rolling = true; + shop.flags().setFlag(data.rollingFlag); + + Collections.shuffle(frames, random); + + rollInteraction = interaction; + lastFrameTime = System.currentTimeMillis(); + currentFrameIndex = 0; + ticksUntilNextFrame = delayFormula.delay(data.frameCount, currentFrameIndex); + + ShopInteractor.handle(rollStartInteractors, interaction); + return true; + } + + @Override + public void tick(long time) { + ShopInteractor.tick(rollStartInteractors, time); + ShopInteractor.tick(mismatchedPlayerInteractors, time); + ShopInteractor.tick(whileRollingInteractors, time); + ShopInteractor.tick(timeoutExpiredInteractors, time); + ShopInteractor.tick(itemClaimedInteractors, time); + ShopInteractor.tick(endInteractors, time); + + for (SlotMachineFrame frame : frames) { + ShopInteractor.tick(frame.interactors(), time); + } + + if (!rolling) { + return; + } + + if (!doneRolling) { + long ticksSinceLastFrame = (time - lastFrameTime) / MinecraftServer.TICK_MS; + if (ticksSinceLastFrame >= ticksUntilNextFrame) { + displayRollFrame(currentFrameIndex); + + lastFrameTime = time; + currentFrameIndex++; + ticksUntilNextFrame = delayFormula.delay(data.frameCount, currentFrameIndex); + } + } + + if (currentFrameIndex < data.frameCount) { + return; + } + + if (!doneRolling) { + rollFinishTime = time; + + TagResolver rolledItemPlaceholder = Placeholder.component("rolled_item", + getItemName(frames.get((currentFrameIndex - 1) % frames.size()).getVisual())); + if (!frames.isEmpty()) { + if (!rollInteraction.player().hasQuit()) { + rollInteraction.player().getPlayer().ifPresent(player -> { + Component message = + MiniMessage.miniMessage().deserialize(data.itemRolledToSelfFormat, rolledItemPlaceholder); + player.sendMessage(message); + }); + } + + rollInteraction.player().module().getPlayerView().getDisplayName().thenAccept(displayName -> { + Set players = new HashSet<>(shop.instance().getPlayers()); + rollInteraction.player().module().getPlayerView().getPlayer().ifPresent(players::remove); + Audience filteredAudience = Audience.audience(players); + + TagResolver rollerPlaceholder = Placeholder.component("roller", displayName); + Component message = MiniMessage.miniMessage() + .deserialize(data.itemRolledToOthersFormat, rollerPlaceholder, rolledItemPlaceholder); + filteredAudience.sendMessage(message); + }); + } + } + + doneRolling = true; + long ticksSinceDoneRolling = (time - rollFinishTime) / MinecraftServer.TICK_MS; + + if (ticksSinceDoneRolling < data.gracePeriodTicks) { + String timeString = tickFormatter.format(data.gracePeriodTicks - ticksSinceDoneRolling); + TagResolver[] tags = + getTagsForFrame(frames.isEmpty() ? null : frames.get((currentFrameIndex - 1) % frames.size()), + Placeholder.unparsed("time_left", timeString)); + + List newComponents = new ArrayList<>(data.gracePeriodFormats.size()); + for (String formatString : data.gracePeriodFormats) { + newComponents.add(MiniMessage.miniMessage().deserialize(formatString, tags)); + } + + updateHologram(newComponents); + return; + } + + if (!rollInteraction.player().hasQuit()) { + ShopInteractor.handle(timeoutExpiredInteractors, rollInteraction); + } + + reset(); + } + + private void reset() { + ShopInteractor.handle(endInteractors, rollInteraction); + + if (item != null) { + item.remove(); + item = null; + } + + hologram.clear(); + rollInteraction = null; + rolling = false; + shop.flags().clearFlag(data.rollingFlag); + doneRolling = false; + rollFinishTime = 0L; + lastFrameTime = 0L; + currentFrameIndex = 0; + ticksUntilNextFrame = 0; + } + + private void displayRollFrame(int index) { + if (frames.isEmpty()) { + return; + } + + SlotMachineFrame frame = frames.get(index % frames.size()); + + if (item != null) { + item.remove(); + } + + item = new Entity(EntityType.ITEM); + ItemEntityMeta meta = (ItemEntityMeta)item.getEntityMeta(); + meta.setItem(frame.getVisual()); + meta.setHasNoGravity(true); + + item.setInstance(shop.instance(), shop.center().add(0, data.itemOffset, 0)); + + TagResolver[] tags = getTagsForFrame(frame); + List newComponents = new ArrayList<>(data.frameHologramFormats.size()); + for (String formatString : data.frameHologramFormats) { + newComponents.add(MiniMessage.miniMessage().deserialize(formatString, tags)); + } + + updateHologram(newComponents); + } + + private TagResolver[] getTagsForFrame(@Nullable SlotMachineFrame frame, TagResolver... additionalTags) { + TagResolver rollingPlayerTag = Placeholder.component("rolling_player", + rollInteraction.player().module().getPlayerView().getDisplayNameIfPresent()); + + Component displayName = frame != null ? getItemName(frame.getVisual()) : Component.empty(); + TagResolver itemName = Placeholder.component("item_name", displayName); + + TagResolver[] tags = new TagResolver[additionalTags.length + 2]; + tags[0] = rollingPlayerTag; + tags[1] = itemName; + + System.arraycopy(additionalTags, 0, tags, 2, additionalTags.length); + return tags; + } + + private Component getItemName(ItemStack itemStack) { + Component displayName = itemStack.getDisplayName(); + if (displayName == null) { + displayName = Component.translatable(itemStack.material().registry().translationKey()); + } + + return displayName; + } + + private void updateHologram(List newComponents) { + while (hologram.size() > newComponents.size()) { + hologram.remove(hologram.size() - 1); + } + + for (int i = 0; i < newComponents.size(); i++) { + Component newComponent = newComponents.get(i); + Component oldComponent = i < hologram.size() ? hologram.get(i) : null; + + if (!newComponent.equals(oldComponent)) { + if (oldComponent == null) { + hologram.add(newComponent); + continue; + } + + hologram.set(i, newComponent); + } + } + } + + public interface SlotMachineFrame { + @NotNull ItemStack getVisual(); + + @NotNull List interactors(); + } + + public interface DelayFormula { + int delay(int frameCount, int frame); + } + + @Model("zombies.map.shop.interactor.slot_machine.delay.constant") + @Cache(false) + public static class ConstantDelayFormula implements DelayFormula { + private final Data data; + + @FactoryMethod + public ConstantDelayFormula(@NotNull Data data) { + this.data = data; + } + + @Override + public int delay(int frameCount, int frame) { + return data.delay; + } + + @DataObject + public record Data(int delay) { + } + } + + @Model("zombies.map.shop.interactor.slot_machine.delay.linear") + @Cache(false) + public static class LinearDelayFormula implements DelayFormula { + private final Data data; + + @FactoryMethod + public LinearDelayFormula(@NotNull Data data) { + this.data = data; + } + + @Override + public int delay(int frameCount, int frame) { + return (int)Math.rint( + ((double)(data.endDelay - data.startDelay) / (double)frameCount) * frame + data.startDelay); + } + + @DataObject + public record Data(int startDelay, int endDelay) { + } + } + + @Model("zombies.map.shop.interactor.slot_machine.frame") + @Cache(false) + public static class BasicSlotMachineFrame implements SlotMachineFrame { + private final Data data; + private final List interactors; + + @FactoryMethod + public BasicSlotMachineFrame(@NotNull Data data, + @NotNull @Child("interactors") List interactors) { + this.data = data; + this.interactors = interactors; + } + + @Override + public @NotNull ItemStack getVisual() { + return data.itemStack; + } + + @Override + public @NotNull List interactors() { + return interactors; + } + + @DataObject + public record Data(@NotNull ItemStack itemStack, @NotNull @ChildPath("interactors") List interactors) { + } + } + + @DataObject + public record Data(int frameCount, + double hologramOffset, + double itemOffset, + int gracePeriodTicks, + @NotNull String itemRolledToSelfFormat, + @NotNull String itemRolledToOthersFormat, + @NotNull String itemClaimedFormat, + @NotNull List frameHologramFormats, + @NotNull Key rollingFlag, + @NotNull List gracePeriodFormats, + @NotNull Evaluation evaluation, + @NotNull @ChildPath("roll_predicates") List rollPredicates, + @NotNull @ChildPath("tick_formatter") String tickFormatter, + @NotNull @ChildPath("delay_formula") String delayFormula, + @NotNull @ChildPath("frames") List frames, + @NotNull @ChildPath("roll_fail_interactors") List rollFailInteractors, + @NotNull @ChildPath("roll_start_interactors") List rollStartInteractors, + @NotNull @ChildPath("mismatched_player_interactors") List mismatchedPlayerInteractors, + @NotNull @ChildPath("while_rolling_interactors") List whileRollingInteractors, + @NotNull @ChildPath("timeout_expired_interactors") List timeoutExpiredInteractors, + @NotNull @ChildPath("item_claimed_interactors") List itemClaimedInteractors, + @NotNull @ChildPath("end_interactors") List endInteractors) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SubstitutedTitleInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SubstitutedTitleInteractor.java index f0882064d..5db5e6a57 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SubstitutedTitleInteractor.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/SubstitutedTitleInteractor.java @@ -6,6 +6,8 @@ import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.TitlePart; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; @@ -28,8 +30,9 @@ public boolean handleInteraction(@NotNull PlayerInteraction interaction) { return false; } - String name = interaction.player().getUsername(); - Component message = MiniMessage.miniMessage().deserialize(String.format(data.formatString, name)); + Component playerName = interaction.player().module().getPlayerView().getDisplayNameIfPresent(); + TagResolver playerPlaceholder = Placeholder.component("player", playerName); + Component message = MiniMessage.miniMessage().deserialize(data.format, playerPlaceholder); if (data.broadcast) { shop.instance().sendTitlePart(data.titlePart, message); @@ -47,6 +50,6 @@ public void initialize(@NotNull Shop shop) { } @DataObject - public record Data(@NotNull String formatString, boolean broadcast, TitlePart titlePart) { + public record Data(@NotNull String format, boolean broadcast, TitlePart titlePart) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/UpgradeEquipmentInteractor.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/UpgradeEquipmentInteractor.java new file mode 100644 index 000000000..a3cb63c9e --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/interactor/UpgradeEquipmentInteractor.java @@ -0,0 +1,119 @@ +package org.phantazm.zombies.map.shop.interactor; + +import com.github.steanky.element.core.annotation.*; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.equipment.Equipment; +import org.phantazm.core.equipment.Upgradable; +import org.phantazm.core.equipment.UpgradePath; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; +import org.phantazm.zombies.player.ZombiesPlayer; + +import java.util.List; +import java.util.Optional; + +@Model("zombies.map.shop.interactor.upgrade_equipment") +@Cache(false) +public class UpgradeEquipmentInteractor implements ShopInteractor { + private final Data data; + private final UpgradePath upgradePath; + private final List notUpgradableInteractors; + private final List noHeldEquipmentInteractors; + private final List noUpgradeInteractors; + private final List upgradeInteractors; + + @FactoryMethod + public UpgradeEquipmentInteractor(@NotNull Data data, @NotNull @Child("upgrade_path") UpgradePath upgradePath, + @NotNull @Child("not_upgradable_interactors") List notUpgradableInteractors, + @NotNull @Child("no_held_equipment_interactors") List noHeldEquipmentInteractors, + @NotNull @Child("no_upgrade_interactors") List noUpgradeInteractors, + @NotNull @Child("upgrade_interactors") List upgradeInteractors) { + this.data = data; + this.upgradePath = upgradePath; + this.notUpgradableInteractors = notUpgradableInteractors; + this.noHeldEquipmentInteractors = noHeldEquipmentInteractors; + this.noUpgradeInteractors = noUpgradeInteractors; + this.upgradeInteractors = upgradeInteractors; + } + + @Override + public void initialize(@NotNull Shop shop) { + ShopInteractor.initialize(notUpgradableInteractors, shop); + ShopInteractor.initialize(noHeldEquipmentInteractors, shop); + ShopInteractor.initialize(noUpgradeInteractors, shop); + ShopInteractor.initialize(upgradeInteractors, shop); + } + + @Override + public boolean handleInteraction(@NotNull PlayerInteraction interaction) { + ZombiesPlayer zombiesPlayer = interaction.player(); + Optional heldEquipmentOptional = zombiesPlayer.getHeldEquipment(); + if (heldEquipmentOptional.isEmpty()) { + ShopInteractor.handle(noHeldEquipmentInteractors, interaction); + return false; + } + + Equipment equipment = heldEquipmentOptional.get(); + if (!(equipment instanceof Upgradable upgradable)) { + ShopInteractor.handle(notUpgradableInteractors, interaction); + return false; + } + + Optional upgradeKeyOptional = upgradePath.nextUpgrade(upgradable.currentLevel()); + if (upgradeKeyOptional.isEmpty()) { + ShopInteractor.handle(noUpgradeInteractors, interaction); + return false; + } + + Key upgradeKey = upgradeKeyOptional.get(); + if (!upgradable.getSuggestedUpgrades().contains(upgradeKey)) { + ShopInteractor.handle(notUpgradableInteractors, interaction); + return false; + } + + ItemStack previousItemStack = equipment.getItemStack(); + Component previousComponent = previousItemStack.getDisplayName(); + if (previousComponent == null) { + previousComponent = Component.translatable(previousItemStack.material().registry().translationKey()); + } + TagResolver previousTag = Placeholder.component("previous_item", previousComponent); + + upgradable.setLevel(upgradeKey); + + ItemStack itemStack = equipment.getItemStack(); + Component component = itemStack.getDisplayName(); + if (component == null) { + component = Component.translatable(itemStack.material().registry().translationKey()); + } + TagResolver upgradedTag = Placeholder.component("upgraded_item", component); + + zombiesPlayer.getPlayer().ifPresent(player -> player.sendMessage( + MiniMessage.miniMessage().deserialize(data.upgradeFormatMessage, previousTag, upgradedTag))); + + ShopInteractor.handle(upgradeInteractors, interaction); + return true; + } + + @Override + public void tick(long time) { + ShopInteractor.tick(notUpgradableInteractors, time); + ShopInteractor.tick(noHeldEquipmentInteractors, time); + ShopInteractor.tick(noUpgradeInteractors, time); + ShopInteractor.tick(upgradeInteractors, time); + } + + @DataObject + public record Data(@NotNull String upgradeFormatMessage, + @NotNull @ChildPath("upgrade_path") String upgradePath, + @NotNull @ChildPath("not_upgradable_interactors") List notUpgradableInteractors, + @NotNull @ChildPath("no_held_equipment_interactors") List noHeldEquipmentInteractors, + @NotNull @ChildPath("no_upgrade_interactors") List noUpgradeInteractors, + @NotNull @ChildPath("upgrade_interactors") List upgradeInteractors) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentCostPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentCostPredicate.java index 3d4e7d53b..7b9a50f6f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentCostPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentCostPredicate.java @@ -13,6 +13,7 @@ import org.phantazm.zombies.coin.TransactionResult; import org.phantazm.zombies.map.shop.PlayerInteraction; +import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -39,7 +40,8 @@ public boolean canUpgrade(@NotNull PlayerInteraction playerInteraction, @NotNull } TransactionResult result = - coins.runTransaction(new Transaction(transactionModifierSource.modifiers(data.modifier), -cost)); + coins.runTransaction(new Transaction(transactionModifierSource.modifiers(data.modifier), + Collections.emptyList(), -cost)); return result.isAffordable(coins); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentPresentPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentPresentPredicate.java index cd22975d5..469c3ddd5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentPresentPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentPresentPredicate.java @@ -7,6 +7,7 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; @Model("zombies.map.shop.equipment_predicate.present") @Cache(false) @@ -18,7 +19,7 @@ public EquipmentPresentPredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return data.requirePresent == isPresent(interaction); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentSpacePredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentSpacePredicate.java index 5de552176..ba3259c0a 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentSpacePredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentSpacePredicate.java @@ -8,6 +8,7 @@ import org.phantazm.core.equipment.Upgradable; import org.phantazm.core.equipment.UpgradePath; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.ZombiesPlayerModule; @@ -29,7 +30,7 @@ public EquipmentSpacePredicate(@NotNull Data data, @NotNull @Child("upgrade_path } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { ZombiesPlayer player = interaction.player(); ZombiesPlayerModule module = player.module(); EquipmentHandler handler = module.getEquipmentHandler(); diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentTierPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentTierPredicate.java new file mode 100644 index 000000000..9449a6b87 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/EquipmentTierPredicate.java @@ -0,0 +1,44 @@ +package org.phantazm.zombies.map.shop.predicate; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.Tags; +import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; + +import java.util.Optional; + +@Model("zombies.map.shop.predicate.equipment_tier") +@Cache(false) +public class EquipmentTierPredicate implements ShopPredicate { + private final Data data; + + @FactoryMethod + public EquipmentTierPredicate(@NotNull Data data) { + this.data = data; + } + + @Override + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { + Optional playerOptional = interaction.player().getPlayer(); + if (playerOptional.isEmpty()) { + return false; + } + + Player player = playerOptional.get(); + PlayerInventory inventory = player.getInventory(); + ItemStack stack = data.slot == -1 ? inventory.getItemInMainHand() : inventory.getItemStack(data.slot); + int existingTier = stack.getTag(Tags.ARMOR_TIER); + return existingTier <= data.tier; + } + + @DataObject + public record Data(int slot, int tier) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/InteractingPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/InteractingPredicate.java index 26fa724fe..ab00380a5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/InteractingPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/InteractingPredicate.java @@ -3,6 +3,7 @@ import com.github.steanky.element.core.annotation.*; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.interactor.ShopInteractor; import java.util.List; @@ -22,8 +23,8 @@ public InteractingPredicate(@NotNull @Child("delegate") ShopPredicate delegate, } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { - if (delegate.canInteract(interaction)) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { + if (delegate.canInteract(interaction, shop)) { for (ShopInteractor interactor : interactors) { interactor.handleInteraction(interaction); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/MapFlagPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/MapFlagPredicate.java index c1dc3c04d..9b8553a80 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/MapFlagPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/MapFlagPredicate.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.shop.predicate; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -7,10 +8,12 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.Flaggable; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import java.util.Objects; @Model("zombies.map.shop.predicate.map_flag") +@Cache(false) public class MapFlagPredicate extends PredicateBase { private final Flaggable flaggable; @@ -21,7 +24,7 @@ public MapFlagPredicate(@NotNull Data data, @NotNull Flaggable flaggable) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return flaggable.hasFlag(data.flag) != data.requireAbsent; } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerFlagPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerFlagPredicate.java index 6c1e86732..a8d39028e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerFlagPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerFlagPredicate.java @@ -6,6 +6,7 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; @Model("zombies.map.shop.predicate.player_flag") public class PlayerFlagPredicate extends PredicateBase { @@ -16,7 +17,7 @@ public PlayerFlagPredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return interaction.player().flags().hasFlag(data.flag) != data.requireAbsent; } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerInGamePredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerInGamePredicate.java index 1024c7fe4..55365a717 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerInGamePredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerInGamePredicate.java @@ -5,6 +5,7 @@ import com.github.steanky.element.core.annotation.Model; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; @Model("zombies.map.shop.predicate.in_game") public class PlayerInGamePredicate extends PredicateBase { @@ -14,7 +15,7 @@ public PlayerInGamePredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return interaction.player().module().getMeta().isInGame(); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerStatePredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerStatePredicate.java index 0998b47bb..5061ad504 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerStatePredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/PlayerStatePredicate.java @@ -6,6 +6,7 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import java.util.Set; @@ -17,7 +18,7 @@ public PlayerStatePredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return data.blacklist != data.states.contains(interaction.player().module().getStateSwitcher().getState().key()); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/ShopPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/ShopPredicate.java index fcd6c8c80..f88f38320 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/ShopPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/ShopPredicate.java @@ -2,14 +2,15 @@ import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; -import java.util.function.Predicate; +import java.util.function.BiPredicate; -public interface ShopPredicate extends Predicate { - boolean canInteract(@NotNull PlayerInteraction interaction); +public interface ShopPredicate extends BiPredicate { + boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop); @Override - default boolean test(PlayerInteraction interaction) { - return canInteract(interaction); + default boolean test(PlayerInteraction interaction, Shop shop) { + return canInteract(interaction, shop); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/StaticCostPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/StaticCostPredicate.java index 119ef6693..c7ee731f3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/StaticCostPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/StaticCostPredicate.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.map.shop.predicate; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -9,10 +10,12 @@ import org.phantazm.zombies.coin.Transaction; import org.phantazm.zombies.coin.TransactionModifierSource; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import java.util.Objects; @Model("zombies.map.shop.predicate.static_cost") +@Cache(false) public class StaticCostPredicate extends PredicateBase { private final TransactionModifierSource transactionModifierSource; @@ -23,7 +26,7 @@ public StaticCostPredicate(@NotNull Data data, @NotNull TransactionModifierSourc } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { PlayerCoins coins = interaction.player().module().getCoins(); return coins.runTransaction(new Transaction(transactionModifierSource.modifiers(data.modifierType), -data.cost)) .isAffordable(coins); diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/TypePredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/TypePredicate.java index 323fa4744..368575422 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/TypePredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/TypePredicate.java @@ -6,6 +6,7 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import java.util.Set; @@ -17,7 +18,7 @@ public TypePredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return data.types.contains(interaction.key()) != data.blacklist; } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/UuidPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/UuidPredicate.java index 88b5dfbef..a57615960 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/UuidPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/UuidPredicate.java @@ -1,15 +1,18 @@ package org.phantazm.zombies.map.shop.predicate; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import java.util.Set; import java.util.UUID; @Model("zombies.map.shop.predicate.uuid") +@Cache(false) public class UuidPredicate extends PredicateBase { @FactoryMethod public UuidPredicate(@NotNull Data data) { @@ -17,7 +20,7 @@ public UuidPredicate(@NotNull Data data) { } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { return data.blacklist != data.uuids.contains(interaction.player().getUUID()); } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/AndPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/AndPredicate.java index 750b2237e..98c8bb1b2 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/AndPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/AndPredicate.java @@ -1,14 +1,19 @@ package org.phantazm.zombies.map.shop.predicate.logic; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.predicate.PredicateBase; import org.phantazm.zombies.map.shop.predicate.ShopPredicate; import java.util.List; @Model("zombies.map.shop.predicate.and") +@Cache(false) public class AndPredicate extends PredicateBase { private final List predicates; @@ -19,10 +24,10 @@ public AndPredicate(@NotNull Data data, @Child("predicates") List } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { boolean failed = false; for (ShopPredicate predicate : predicates) { - if (!predicate.canInteract(interaction)) { + if (!predicate.canInteract(interaction, shop)) { failed = true; if (data.shortCircuit) { @@ -36,5 +41,9 @@ public boolean canInteract(@NotNull PlayerInteraction interaction) { @DataObject public record Data(boolean shortCircuit, @NotNull @ChildPath("predicates") List paths) { + @Default("shortCircuit") + public static @NotNull ConfigElement shortCircuitDefault() { + return ConfigPrimitive.of(true); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/NotPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/NotPredicate.java index aa59016be..f5ef4ddbc 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/NotPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/NotPredicate.java @@ -3,12 +3,14 @@ import com.github.steanky.element.core.annotation.*; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.predicate.PredicateBase; import org.phantazm.zombies.map.shop.predicate.ShopPredicate; import java.util.Objects; @Model("zombies.map.shop.predicate.not") +@Cache(false) public class NotPredicate extends PredicateBase { private final ShopPredicate predicate; @@ -19,8 +21,8 @@ public NotPredicate(@NotNull Data data, @Child("predicate") ShopPredicate predic } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { - return !predicate.canInteract(interaction); + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { + return !predicate.canInteract(interaction, shop); } @DataObject diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/OrPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/OrPredicate.java index a78194a5d..a73888fdb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/OrPredicate.java +++ b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/OrPredicate.java @@ -1,14 +1,19 @@ package org.phantazm.zombies.map.shop.predicate.logic; import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.shop.PlayerInteraction; +import org.phantazm.zombies.map.shop.Shop; import org.phantazm.zombies.map.shop.predicate.PredicateBase; import org.phantazm.zombies.map.shop.predicate.ShopPredicate; import java.util.List; @Model("zombies.map.shop.predicate.or") +@Cache(false) public class OrPredicate extends PredicateBase { private final List predicates; @@ -19,10 +24,10 @@ public OrPredicate(@NotNull Data data, @Child("predicates") List } @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { + public boolean canInteract(@NotNull PlayerInteraction interaction, @NotNull Shop shop) { boolean succeeded = false; for (ShopPredicate predicate : predicates) { - if (predicate.canInteract(interaction)) { + if (predicate.canInteract(interaction, shop)) { succeeded = true; if (data.shortCircuit) { @@ -36,5 +41,9 @@ public boolean canInteract(@NotNull PlayerInteraction interaction) { @DataObject public record Data(boolean shortCircuit, @NotNull @ChildPath("predicates") List paths) { + @Default("shortCircuit") + public static @NotNull ConfigElement shortCircuitDefault() { + return ConfigPrimitive.of(true); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/XorPredicate.java b/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/XorPredicate.java deleted file mode 100644 index bd20b42ed..000000000 --- a/zombies/src/main/java/org/phantazm/zombies/map/shop/predicate/logic/XorPredicate.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.phantazm.zombies.map.shop.predicate.logic; - -import com.github.steanky.element.core.annotation.*; -import org.jetbrains.annotations.NotNull; -import org.phantazm.zombies.map.shop.PlayerInteraction; -import org.phantazm.zombies.map.shop.predicate.PredicateBase; -import org.phantazm.zombies.map.shop.predicate.ShopPredicate; - -import java.util.List; - -@Model("zombies.map.shop.predicate.xor") -public class XorPredicate extends PredicateBase { - private final List predicates; - - @FactoryMethod - public XorPredicate(@NotNull Data data, @Child("predicates") List predicates) { - super(data); - this.predicates = List.copyOf(predicates); - } - - @Override - public boolean canInteract(@NotNull PlayerInteraction interaction) { - int successes = 0; - for (ShopPredicate predicate : predicates) { - if (predicate.canInteract(interaction)) { - if (++successes > 1 && data.shortCircuit) { - return false; - } - } - } - - return successes == 1; - } - - @DataObject - public record Data(boolean shortCircuit, @NotNull @ChildPath("predicates") List paths) { - } -} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/BasicMobSpawner.java b/zombies/src/main/java/org/phantazm/zombies/mob/BasicMobSpawner.java index a642844b6..5497abc0e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/mob/BasicMobSpawner.java +++ b/zombies/src/main/java/org/phantazm/zombies/mob/BasicMobSpawner.java @@ -15,19 +15,25 @@ import com.github.steanky.ethylene.core.collection.LinkedConfigNode; import com.github.steanky.ethylene.core.processor.ConfigProcessException; import com.github.steanky.ethylene.core.processor.ConfigProcessor; +import com.github.steanky.toolkit.collection.Wrapper; import it.unimi.dsi.fastutil.booleans.BooleanObjectPair; import it.unimi.dsi.fastutil.objects.Object2FloatArrayMap; import net.kyori.adventure.key.Key; import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeModifier; +import net.minestom.server.attribute.AttributeOperation; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.metadata.EntityMeta; import net.minestom.server.instance.Instance; import net.minestom.server.item.ItemStack; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.phantazm.core.ElementUtils; +import org.phantazm.core.tracker.BoundedTracker; import org.phantazm.mob.BasicPhantazmMob; import org.phantazm.mob.MobModel; import org.phantazm.mob.MobStore; @@ -37,6 +43,8 @@ import org.phantazm.mob.spawner.MobSpawner; import org.phantazm.proxima.bindings.minestom.ProximaEntity; import org.phantazm.proxima.bindings.minestom.Spawner; +import org.phantazm.zombies.ExtraNodeKeys; +import org.phantazm.zombies.map.Window; import org.phantazm.zombies.map.objects.MapObjects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +73,7 @@ public class BasicMobSpawner implements MobSpawner { private final Spawner proximaSpawner; private final KeyParser keyParser; private final MobStore mobStore; + private final Supplier mapObjects; private final DependencyProvider mobDependencyProvider; /** @@ -79,15 +88,33 @@ public BasicMobSpawner(@NotNull Map, ConfigProcessor init) { + ProximaEntity proximaEntity = + proximaSpawner.spawn(instance, pos, model.getEntityType(), model.getFactory(), entity -> { + init.accept(entity); + + BoundedTracker windowTracker = mapObjects.get().windowTracker(); + entity.pathfinding().setPenalty((x, y, z, h) -> { + Optional windowOptional = windowTracker.atPoint(x, y + 1, z); + + //noinspection OptionalIsPresent + if (windowOptional.isEmpty()) { + return h; + } + + return windowOptional.get().isBlockBroken(x, y + 1, z) ? h : h * 10; + }); + }); + + setTasks(proximaEntity, model); setEntityMeta(proximaEntity, model); setEquipment(proximaEntity, model); setAttributes(proximaEntity, model); @@ -118,6 +145,40 @@ public BasicMobSpawner(@NotNull Map, ConfigProcessor= 0) { + proximaEntity.scheduler() + .scheduleTask(proximaEntity::kill, TaskSchedule.tick(ticksUntilDeath), TaskSchedule.stop()); + } + + if (speedupIncrements > 0) { + Wrapper taskWrapper = Wrapper.ofNull(); + Task speedupTask = proximaEntity.scheduler().scheduleTask(new Runnable() { + private int times = 0; + + @Override + public void run() { + if (++times == speedupIncrements) { + taskWrapper.get().cancel(); + } + + UUID modifierUUID = UUID.randomUUID(); + proximaEntity.getAttribute(Attribute.MOVEMENT_SPEED).addModifier( + new AttributeModifier(modifierUUID, modifierUUID.toString(), speedupAmount, + AttributeOperation.MULTIPLY_BASE)); + } + }, TaskSchedule.tick(speedupInterval), TaskSchedule.tick(speedupInterval)); + taskWrapper.set(speedupTask); + } + } + private void setEntityMeta(@NotNull ProximaEntity neuralEntity, @NotNull MobModel model) { EntityMeta meta = neuralEntity.getEntityMeta(); ConfigNode metaNode = model.getMetaNode(); @@ -251,8 +312,12 @@ public Random getRandom() { return random; } - public @NotNull Supplier mapObjects() { + public @NotNull Supplier mapObjectsSupplier() { return mapObjects; } + + public @NotNull MapObjects mapObjects() { + return mapObjects.get(); + } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/goal/BreakNearbyWindowGoal.java b/zombies/src/main/java/org/phantazm/zombies/mob/goal/BreakNearbyWindowGoal.java index 3f7367a3f..df05e8254 100644 --- a/zombies/src/main/java/org/phantazm/zombies/mob/goal/BreakNearbyWindowGoal.java +++ b/zombies/src/main/java/org/phantazm/zombies/mob/goal/BreakNearbyWindowGoal.java @@ -14,10 +14,12 @@ import org.phantazm.mob.goal.GoalCreator; import org.phantazm.proxima.bindings.minestom.goal.ProximaGoal; import org.phantazm.zombies.event.MobBreakWindowEvent; +import org.phantazm.zombies.map.Room; import org.phantazm.zombies.map.Window; import org.phantazm.zombies.map.objects.MapObjects; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; @@ -35,19 +37,23 @@ public BreakNearbyWindowGoal(@NotNull Data data, @NotNull Supplier windowTracker; + private final BoundedTracker roomTracker; private final PhantazmMob self; private long lastBreakCheck; - private Goal(Data data, BoundedTracker windowTracker, PhantazmMob self) { + private Goal(Data data, BoundedTracker windowTracker, BoundedTracker roomTracker, + PhantazmMob self) { this.data = data; this.windowTracker = windowTracker; + this.roomTracker = roomTracker; this.self = self; this.lastBreakCheck = -1; } @@ -62,21 +68,26 @@ public void tick(long time) { long ticksSinceLastBreak = (time - lastBreakCheck) / MinecraftServer.TICK_MS; LivingEntity entity = self.entity(); if (ticksSinceLastBreak > data.breakTicks) { + Optional roomOptional = roomTracker.atPoint(entity.getPosition()); + if (roomOptional.isPresent()) { + return; + } + BoundingBox boundingBox = entity.getBoundingBox(); - windowTracker.closestInRangeToBounds(entity.getPosition(), boundingBox.width(), 0.5, data.breakRadius) - .ifPresent(window -> { - window.setLastBreakTime(time); + windowTracker.closestInRangeToBounds(entity.getPosition(), boundingBox.width(), boundingBox.height(), + data.breakRadius).ifPresent(window -> { + window.setLastBreakTime(time); - int index = window.getIndex(); - int targetIndex = index - data.breakCount; + int index = window.getIndex(); + int targetIndex = index - data.breakCount; - int amount = window.updateIndex(targetIndex); - if (amount != 0) { - EventDispatcher.call(new MobBreakWindowEvent(self, window, -amount)); - entity.swingMainHand(); - } - }); + int amount = window.updateIndex(targetIndex); + if (amount != 0) { + EventDispatcher.call(new MobBreakWindowEvent(self, window, -amount)); + entity.swingMainHand(); + } + }); lastBreakCheck = time; } diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/AttributeModifyingSkill.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/AttributeModifyingSkill.java new file mode 100644 index 000000000..1de78f998 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/AttributeModifyingSkill.java @@ -0,0 +1,104 @@ +package org.phantazm.zombies.mob.skill; + +import com.github.steanky.element.core.annotation.*; +import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeModifier; +import net.minestom.server.attribute.AttributeOperation; +import net.minestom.server.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.skill.Skill; +import org.phantazm.mob.target.TargetSelector; +import org.phantazm.zombies.Attributes; +import org.phantazm.zombies.map.objects.MapObjects; +import org.phantazm.zombies.player.ZombiesPlayer; + +import java.util.*; + +@Model("mob.skill.attribute_modifying") +@Cache(false) +public class AttributeModifyingSkill implements Skill { + private final Data data; + private final UUID uuid; + private final String uuidString; + private final Attribute attribute; + private final TargetSelector targetSelector; + + private final Map playerMap; + + private final Set affectedEntities; + + @FactoryMethod + public AttributeModifyingSkill(@NotNull Data data, @NotNull @Child("selector") TargetSelector targetSelector, + @NotNull MapObjects mapObjects) { + this.data = data; + this.uuid = UUID.randomUUID(); + this.uuidString = this.uuid.toString(); + this.attribute = Objects.requireNonNullElse(Attribute.fromKey(data.attribute), Attributes.NIL); + this.targetSelector = targetSelector; + this.playerMap = mapObjects.module().playerMap(); + this.affectedEntities = Collections.newSetFromMap(new WeakHashMap<>()); + } + + @Override + public void use(@NotNull PhantazmMob self) { + Optional targetOptional = targetSelector.selectTarget(self); + if (targetOptional.isEmpty()) { + return; + } + + Object target = targetOptional.get(); + if (target instanceof LivingEntity entity) { + applyToEntity(entity); + } + else if (target instanceof Iterable iterable) { + for (Object object : iterable) { + if (object instanceof LivingEntity livingEntity) { + applyToEntity(livingEntity); + } + } + } + } + + private void applyToEntity(LivingEntity entity) { + ZombiesPlayer zombiesPlayer = playerMap.get(entity.getUuid()); + + if (zombiesPlayer != null) { + zombiesPlayer.cancellableAttribute(attribute, + new AttributeModifier(uuid, uuidString, data.amount, data.attributeOperation)); + affectedEntities.add(entity); + return; + } + + entity.getAttribute(attribute) + .addModifier(new AttributeModifier(uuid, uuidString, data.amount, data.attributeOperation)); + affectedEntities.add(entity); + } + + private void removeFromEntity(LivingEntity livingEntity) { + ZombiesPlayer zombiesPlayer = playerMap.get(livingEntity.getUuid()); + + if (zombiesPlayer != null) { + zombiesPlayer.removeCancellable(uuid); + return; + } + + livingEntity.getAttribute(attribute).removeModifier(uuid); + } + + @Override + public void end(@NotNull PhantazmMob self) { + for (LivingEntity entity : affectedEntities) { + removeFromEntity(entity); + } + + affectedEntities.clear(); + } + + @DataObject + public record Data(@NotNull String attribute, + float amount, + @NotNull AttributeOperation attributeOperation, + @NotNull @ChildPath("selector") String selector) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/ShootProjectileSkill.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/ShootProjectileSkill.java new file mode 100644 index 000000000..e417c3ace --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/ShootProjectileSkill.java @@ -0,0 +1,179 @@ +package org.phantazm.zombies.mob.skill; + +import com.github.steanky.element.core.annotation.*; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.MobModel; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.goal.CollectionGoalGroup; +import org.phantazm.mob.goal.ProjectileMovementGoal; +import org.phantazm.mob.skill.Skill; +import org.phantazm.mob.target.TargetSelector; +import org.phantazm.mob.validator.TargetValidator; +import org.phantazm.proxima.bindings.minestom.ProximaEntity; +import org.phantazm.zombies.Tags; +import org.phantazm.zombies.map.objects.MapObjects; +import org.phantazm.zombies.mob.skill.hit_action.ProjectileHitEntityAction; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Model("zombies.mob.skill.shoot_projectile") +@Cache(false) +public class ShootProjectileSkill implements Skill { + private final Data data; + private final MapObjects mapObjects; + private final TargetSelector targetSelector; + private final TargetValidator targetValidator; + private final TargetValidator hitValidator; + private final List actions; + + private final UUID uuid; + + @FactoryMethod + public ShootProjectileSkill(@NotNull Data data, @NotNull MapObjects mapObjects, + @NotNull @Child("target_selector") TargetSelector targetSelector, + @NotNull @Child("target_validator") TargetValidator targetValidator, + @NotNull @Child("hit_validator") TargetValidator hitValidator, + @NotNull @Child("actions") List actions) { + this.data = data; + this.mapObjects = mapObjects; + this.targetSelector = targetSelector; + this.targetValidator = targetValidator; + this.hitValidator = hitValidator; + this.actions = actions; + + this.uuid = UUID.randomUUID(); + + EventNode eventNode = mapObjects.module().eventNode().get(); + + eventNode.addListener(ProjectileCollideWithEntityEvent.class, this::onCollideWithEntity); + eventNode.addListener(ProjectileCollideWithBlockEvent.class, this::onCollideWithBlock); + } + + private void onCollideWithBlock(ProjectileCollideWithBlockEvent event) { + Entity projectile = event.getEntity(); + + UUID identifier = projectile.getTag(Tags.SKILL_IDENTIFIER); + if (identifier == null || !identifier.equals(uuid)) { + return; + } + + killOrRemove(projectile); + } + + private void onCollideWithEntity(ProjectileCollideWithEntityEvent event) { + Entity projectile = event.getEntity(); + + UUID shooter = projectile.getTag(Tags.PROJECTILE_SHOOTER); + if (shooter == null) { + return; + } + + UUID identifier = projectile.getTag(Tags.SKILL_IDENTIFIER); + if (identifier == null || !identifier.equals(uuid)) { + return; + } + + PhantazmMob shootingMob = mapObjects.module().mobStore().getMob(shooter); + if (!hitValidator.valid(shootingMob == null ? null : shootingMob.entity(), event.getTarget())) { + return; + } + + for (ProjectileHitEntityAction action : actions) { + action.perform(shootingMob, projectile, event.getTarget()); + } + + killOrRemove(projectile); + } + + private void killOrRemove(Entity entity) { + if (entity instanceof LivingEntity livingEntity) { + livingEntity.kill(); + } + else { + entity.remove(); + } + } + + @Override + public void use(@NotNull PhantazmMob self) { + Instance instance = self.entity().getInstance(); + if (instance == null) { + return; + } + + Optional targetOptional = targetSelector.selectTarget(self); + if (targetOptional.isEmpty()) { + return; + } + + MobModel model = mapObjects.module().mobModelFunction().apply(data.entity); + if (model == null) { + return; + } + + ProximaEntity selfEntity = self.entity(); + Entity targetEntity = targetOptional.get(); + if (!targetValidator.valid(selfEntity, targetEntity)) { + return; + } + + Pos selfPosition = selfEntity.getPosition(); + PhantazmMob mob = mapObjects.mobSpawner() + .spawn(mapObjects.module().instance(), selfPosition.add(0, selfEntity.getEyeHeight(), 0), model); + if (data.addToRound) { + mapObjects.module().roundHandlerSupplier().get().currentRound().ifPresent(round -> { + round.addMob(mob); + }); + } + + ProximaEntity mobEntity = mob.entity(); + + mobEntity.setTag(Tags.PROJECTILE_SHOOTER, selfEntity.getUuid()); + mobEntity.setTag(Tags.SKILL_IDENTIFIER, uuid); + + mobEntity.setNoGravity(!data.gravity); + mobEntity.addGoalGroup(new CollectionGoalGroup(List.of(new ProjectileMovementGoal(mob.entity(), selfEntity, + targetEntity.getPosition().add(0, targetEntity.getBoundingBox().height() / 2, 0), data.power(), + data.spread())))); + } + + @DataObject + public record Data(@NotNull Key entity, + double power, + double spread, + boolean gravity, + boolean addToRound, + @NotNull @ChildPath("target_selector") String targetSelector, + @NotNull @ChildPath("target_validator") String targetValidator, + @NotNull @ChildPath("hit_validator") String hitValidator, + @NotNull @ChildPath("actions") List hitEntityActions) { + @Default("spread") + public static @NotNull ConfigElement defaultSpread() { + return ConfigPrimitive.of(0.0D); + } + + @Default("gravity") + public static @NotNull ConfigElement defaultGravity() { + return ConfigPrimitive.of(true); + } + + @Default("addToRound") + public static @NotNull ConfigElement defaultAddToRound() { + return ConfigPrimitive.of(false); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/SummonMobSkill.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/SummonMobSkill.java new file mode 100644 index 000000000..e2ffeaf19 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/SummonMobSkill.java @@ -0,0 +1,103 @@ +package org.phantazm.zombies.mob.skill; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.kyori.adventure.key.Key; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.tag.Tag; +import org.jetbrains.annotations.NotNull; +import org.phantazm.mob.MobModel; +import org.phantazm.mob.MobStore; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.mob.skill.Skill; +import org.phantazm.mob.spawner.MobSpawner; +import org.phantazm.zombies.event.PhantazmMobDeathEvent; +import org.phantazm.zombies.map.objects.MapObjects; + +import java.util.UUID; + +@Model("zombies.mob.skill.summon_mob") +@Cache(false) +public class SummonMobSkill implements Skill { + private final Data data; + private final MobSpawner mobSpawner; + private final MobStore mobStore; + + private final Tag mobCountTag; + private final Tag ownerUUID; + + private final MapObjects mapObjects; + + @FactoryMethod + public SummonMobSkill(@NotNull Data data, @NotNull MobSpawner mobSpawner, @NotNull MobStore mobStore, + @NotNull MapObjects mapObjects) { + this.data = data; + this.mobSpawner = mobSpawner; + + UUID uuid = UUID.randomUUID(); + this.mobCountTag = Tag.Integer("mob_count_" + uuid).defaultValue(0); + this.ownerUUID = Tag.UUID("owner_" + uuid); + this.mobStore = mobStore; + + this.mapObjects = mapObjects; + + EventNode node = mapObjects.module().eventNode().get(); + node.addListener(PhantazmMobDeathEvent.class, this::onMobDeath); + } + + private void onMobDeath(PhantazmMobDeathEvent event) { + PhantazmMob mob = mobStore.getMob(event.getEntity().getUuid()); + if (mob == null) { + return; + } + + UUID uuid = mob.entity().getTag(ownerUUID); + if (uuid == null) { + return; + } + + PhantazmMob owner = mobStore.getMob(uuid); + if (owner != null) { + owner.entity().tagHandler().updateTag(mobCountTag, value -> value - 1); + } + } + + @Override + public void use(@NotNull PhantazmMob self) { + MobModel model = mapObjects.module().mobModelFunction().apply(data.mob); + if (model == null) { + return; + } + + int spawned = self.entity().getTag(mobCountTag); + for (int i = spawned, j = 0; i < data.maxSpawn && j < data.spawnAmount; i++, j++) { + spawn(self, model); + } + } + + private void spawn(PhantazmMob self, MobModel model) { + PhantazmMob mob = mobSpawner.spawn(mapObjects.module().instance(), self.entity().getPosition(), model); + mob.entity().setTag(ownerUUID, self.entity().getUuid()); + self.entity().tagHandler().updateTag(mobCountTag, value -> value + 1); + + if (data.addToRound) { + mapObjects.module().roundHandlerSupplier().get().currentRound().ifPresent(round -> { + round.addMob(mob); + }); + } + } + + @DataObject + public record Data(@NotNull Key mob, int spawnAmount, int maxSpawn, boolean addToRound) { + @Default("addToRound") + public static @NotNull ConfigElement defaultAddToRound() { + return ConfigPrimitive.of(true); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/AttributeModifierAction.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/AttributeModifierAction.java new file mode 100644 index 000000000..d58585eaf --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/AttributeModifierAction.java @@ -0,0 +1,87 @@ +package org.phantazm.zombies.mob.skill.hit_action; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeModifier; +import net.minestom.server.attribute.AttributeOperation; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.commons.CancellableState; +import org.phantazm.commons.TickTaskScheduler; +import org.phantazm.commons.TickableTask; +import org.phantazm.mob.PhantazmMob; +import org.phantazm.zombies.Attributes; +import org.phantazm.zombies.map.objects.MapObjects; +import org.phantazm.zombies.player.ZombiesPlayer; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Model("zombies.mob.skill.projectile.hit_action.attribute_modifier") +@Cache(false) +public class AttributeModifierAction implements ProjectileHitEntityAction { + private final Data data; + private final TickTaskScheduler taskScheduler; + private final Map playerMap; + + @FactoryMethod + public AttributeModifierAction(@NotNull Data data, @NotNull MapObjects mapObjects) { + this.data = data; + this.taskScheduler = mapObjects.taskScheduler(); + this.playerMap = mapObjects.module().playerMap(); + } + + @Override + public void perform(@Nullable PhantazmMob shooter, @NotNull Entity projectile, @NotNull Entity target) { + if (!(target instanceof LivingEntity livingEntity)) { + return; + } + + ZombiesPlayer zombiesPlayer = playerMap.get(target.getUuid()); + UUID modifier = UUID.randomUUID(); + + if (zombiesPlayer != null) { + zombiesPlayer.registerCancellable(CancellableState.named(modifier, () -> { + zombiesPlayer.getPlayer().ifPresent(player -> { + player.getAttribute(Objects.requireNonNullElse(Attribute.fromKey(data.attribute), Attributes.NIL)) + .addModifier(new AttributeModifier(modifier, modifier.toString(), data.amount, + data.attributeOperation)); + }); + }, () -> { + zombiesPlayer.getPlayer().ifPresent(player -> { + player.getAttribute(Objects.requireNonNullElse(Attribute.fromKey(data.attribute), Attributes.NIL)) + .removeModifier(modifier); + }); + }), true); + + taskScheduler.scheduleTaskNow(TickableTask.afterTicks(data.duration, () -> { + zombiesPlayer.removeCancellable(modifier); + }, () -> { + zombiesPlayer.removeCancellable(modifier); + })); + return; + } + + livingEntity.getAttribute(Objects.requireNonNullElse(Attribute.fromKey(data.attribute), Attributes.NIL)) + .addModifier( + new AttributeModifier(modifier, modifier.toString(), data.amount, data.attributeOperation)); + livingEntity.scheduler().scheduleTask(() -> { + livingEntity.getAttribute(Objects.requireNonNullElse(Attribute.fromKey(data.attribute), Attributes.NIL)) + .removeModifier(modifier); + }, TaskSchedule.tick((int)data.duration), TaskSchedule.stop()); + } + + @DataObject + public record Data(@NotNull String attribute, + float amount, + @NotNull AttributeOperation attributeOperation, + long duration) { + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/DamageAction.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/DamageAction.java new file mode 100644 index 000000000..956c361ba --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/DamageAction.java @@ -0,0 +1,53 @@ +package org.phantazm.zombies.mob.skill.hit_action; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.DataObject; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import com.github.steanky.ethylene.core.ConfigElement; +import com.github.steanky.ethylene.core.ConfigPrimitive; +import com.github.steanky.ethylene.mapper.annotation.Default; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.damage.Damage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.mob.PhantazmMob; + +@Model("zombies.mob.skill.projectile.hit_action.damage") +@Cache +public class DamageAction implements ProjectileHitEntityAction { + private final Data data; + + @FactoryMethod + public DamageAction(@NotNull Data data) { + this.data = data; + } + + @Override + public void perform(@Nullable PhantazmMob shooter, @NotNull Entity projectile, @NotNull Entity target) { + if (!(target instanceof LivingEntity livingEntity)) { + return; + } + + if (livingEntity.damage( + Damage.fromProjectile(shooter == null ? null : shooter.entity(), projectile, data.damage), + data.bypassArmor)) { + double yaw = Math.toRadians(projectile.getPosition().yaw()); + livingEntity.takeKnockback(data.knockback, Math.sin(yaw), -Math.cos(yaw)); + } + } + + @DataObject + public record Data(float damage, float knockback, boolean bypassArmor) { + @Default("bypassArmor") + public static @NotNull ConfigElement defaultBypassArmor() { + return ConfigPrimitive.of(false); + } + + @Default("knockback") + public static @NotNull ConfigElement defaultKnockback() { + return ConfigPrimitive.of(0.4F); + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/ProjectileHitEntityAction.java b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/ProjectileHitEntityAction.java new file mode 100644 index 000000000..202a709e7 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/mob/skill/hit_action/ProjectileHitEntityAction.java @@ -0,0 +1,10 @@ +package org.phantazm.zombies.mob.skill.hit_action; + +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.phantazm.mob.PhantazmMob; + +public interface ProjectileHitEntityAction { + void perform(@Nullable PhantazmMob shooter, @NotNull Entity projectile, @NotNull Entity target); +} diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/validator/MobValidator.java b/zombies/src/main/java/org/phantazm/zombies/mob/validator/MobValidator.java index da098c73a..e4f1f1ded 100644 --- a/zombies/src/main/java/org/phantazm/zombies/mob/validator/MobValidator.java +++ b/zombies/src/main/java/org/phantazm/zombies/mob/validator/MobValidator.java @@ -5,6 +5,7 @@ import com.github.steanky.element.core.annotation.Model; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.phantazm.mob.MobStore; import org.phantazm.mob.validator.TargetValidator; @@ -19,7 +20,7 @@ public MobValidator(@NotNull MobStore mobStore) { } @Override - public boolean valid(@NotNull Entity targeter, @NotNull Entity entity) { + public boolean valid(@Nullable Entity targeter, @NotNull Entity entity) { return mobStore.hasMob(entity.getUuid()); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/mob/validator/ZombiesPlayerValidator.java b/zombies/src/main/java/org/phantazm/zombies/mob/validator/ZombiesPlayerValidator.java index ce7e9b251..7f771e39d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/mob/validator/ZombiesPlayerValidator.java +++ b/zombies/src/main/java/org/phantazm/zombies/mob/validator/ZombiesPlayerValidator.java @@ -7,6 +7,7 @@ import net.kyori.adventure.key.Key; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.phantazm.mob.validator.TargetValidator; import org.phantazm.zombies.map.objects.MapObjects; import org.phantazm.zombies.player.ZombiesPlayer; @@ -27,7 +28,7 @@ public ZombiesPlayerValidator(@NotNull Data data, @NotNull Supplier equipmentCreatorFunction; private final Map mobModelMap; - public BasicZombiesPlayerSource( + private final ContextManager contextManager; + + private final KeyParser keyParser; + + public BasicZombiesPlayerSource(@NotNull ZombiesDatabase database, @NotNull PlayerViewProvider viewProvider, @NotNull Function equipmentCreatorFunction, - @NotNull Map mobModelMap) { + @NotNull Map mobModelMap, @NotNull ContextManager contextManager, + @NotNull KeyParser keyParser) { + this.database = Objects.requireNonNull(database, "database"); + this.viewProvider = Objects.requireNonNull(viewProvider, "viewProvider"); this.equipmentCreatorFunction = Objects.requireNonNull(equipmentCreatorFunction, "equipmentCreatorFunction"); this.mobModelMap = Objects.requireNonNull(mobModelMap, "mobModelMap"); + this.contextManager = Objects.requireNonNull(contextManager, "contextManager"); + this.keyParser = Objects.requireNonNull(keyParser, "keyParser"); } @SuppressWarnings("unchecked") @Override public @NotNull ZombiesPlayer createPlayer(@NotNull ZombiesScene scene, @NotNull Map zombiesPlayers, - @NotNull MapSettingsInfo mapSettingsInfo, @NotNull Instance instance, @NotNull PlayerView playerView, + @NotNull MapSettingsInfo mapSettingsInfo, @NotNull PlayerCoinsInfo playerCoinsInfo, + @NotNull LeaderboardInfo leaderboardInfo, @NotNull Instance instance, @NotNull PlayerView playerView, @NotNull TransactionModifierSource mapTransactionModifierSource, @NotNull Flaggable flaggable, @NotNull EventNode eventNode, @NotNull Random random, @NotNull MapObjects mapObjects, - @NotNull MobStore mobStore, @NotNull MobSpawner mobSpawner, @NotNull CorpseCreator corpseCreator) { + @NotNull MobStore mobStore, @NotNull MobSpawner mobSpawner, @NotNull CorpseCreator corpseCreator, + @NotNull BelowNameTag belowNameTag) { TransactionModifierSource playerTransactionModifierSource = new BasicTransactionModifierSource(); ZombiesPlayerMeta meta = new ZombiesPlayerMeta(); ZombiesPlayerMapStats stats = BasicZombiesPlayerMapStats.createBasicStats(playerView.getUUID(), mapSettingsInfo.id()); - PlayerCoins coins = new BasicPlayerCoins(playerView, stats, new BasicTransactionComponentCreator(), 0); + ZombiesPlayerActionBar actionBar = new ZombiesPlayerActionBar(playerView); + + PlayerCoins coins = new BasicPlayerCoins(stats, + new BasicTransactionMessager(actionBar, MiniMessage.miniMessage(), playerCoinsInfo), 0); PlayerKills kills = new BasicPlayerKills(stats); InventoryProfile livingProfile = new BasicInventoryProfile(45); @@ -117,7 +142,7 @@ public BasicZombiesPlayerSource( } } catch (NBTException e) { - LOGGER.warn("Failed to load item in slot {} because its NBT string is invalid: {}", i, e); + LOGGER.warn("Failed to load item in slot {} because its NBT string is invalid:", i, e); } } @@ -138,24 +163,24 @@ public BasicZombiesPlayerSource( Wrapper zombiesPlayerWrapper = Wrapper.ofNull(); ZombiesEquipmentModule equipmentModule = - new ZombiesEquipmentModule(zombiesPlayers, playerView, stats, mobSpawner, mobStore, eventNode, random, - mapObjects, zombiesPlayerWrapper, mobModelMap::get); + new ZombiesEquipmentModule(zombiesPlayers, playerView, stats, actionBar, mobSpawner, mobStore, + eventNode, random, mapObjects, zombiesPlayerWrapper, mobModelMap::get); EquipmentCreator equipmentCreator = equipmentCreatorFunction.apply(equipmentModule); - Sidebar sidebar = new Sidebar( - Component.text(StringUtils.center("ZOMBIES", 16), NamedTextColor.YELLOW, TextDecoration.BOLD)); + Sidebar sidebar = new Sidebar(mapSettingsInfo.scoreboardHeader()); TabList tabList = new TabList(UUID.randomUUID().toString(), ScoreboardObjectivePacket.Type.INTEGER); - Function aliveStateCreator = unused -> { + Function aliveStateCreator = context -> { return new BasicZombiesPlayerState(Component.text("ALIVE"), ZombiesPlayerStateKeys.ALIVE.key(), - List.of(new BasicAliveStateActivable(accessRegistry, playerView, sidebar, tabList))); + List.of(new BasicAliveStateActivable(context, instance, accessRegistry, playerView, meta, + mapSettingsInfo, sidebar, tabList, belowNameTag))); }; BiFunction, ZombiesPlayerState> deadStateCreator = (context, activables) -> { List combinedActivables = new ArrayList<>(activables); combinedActivables.add( - new BasicDeadStateActivable(accessRegistry, context, instance, playerView, mapSettingsInfo, - sidebar, tabList, stats)); + new BasicDeadStateActivable(accessRegistry, context, instance, playerView, meta, + mapSettingsInfo, sidebar, tabList, belowNameTag, stats)); return new BasicZombiesPlayerState(Component.text("DEAD").color(NamedTextColor.RED), ZombiesPlayerStateKeys.DEAD.key(), combinedActivables); }; @@ -164,8 +189,9 @@ public BasicZombiesPlayerSource( Wrapper corpseWrapper = Wrapper.ofNull(); Supplier deadStateSupplier = () -> { - DeadPlayerStateContext deathContext = DeadPlayerStateContext.killed(context.getKiller().orElse(null), - context.getKnockRoom().orElse(null)); + DeadPlayerStateContext deathContext = + DeadPlayerStateContext.killed(context.getKnockLocation(), context.getKiller().orElse(null), + context.getKnockRoom().orElse(null)); return deadStateCreator.apply(deathContext, List.of(corpseWrapper.get().asDeathActivable(), new Activable() { @Override @@ -176,8 +202,8 @@ public void end() { }; ReviveHandler reviveHandler = - new ReviveHandler(() -> aliveStateCreator.apply(NoContext.INSTANCE), deadStateSupplier, - new NearbyReviverFinder(zombiesPlayers, playerView, mapSettingsInfo.reviveRadius()), 500L); + new ReviveHandler(context, zombiesPlayers.values(), aliveStateCreator, deadStateSupplier, + new NearbyReviverPredicate(playerView, mapSettingsInfo.reviveRadius()), 500L); CorpseCreator.Corpse corpse = corpseCreator.forPlayer(instance, zombiesPlayerWrapper.get(), context.getKnockLocation(), @@ -185,9 +211,9 @@ public void end() { corpseWrapper.set(corpse); return new KnockedPlayerState(reviveHandler, - List.of(new BasicKnockedStateActivable(context, instance, playerView, mapSettingsInfo, - reviveHandler, tickFormatter, sidebar, tabList, stats), corpse.asKnockActivable(), - new Activable() { + List.of(new BasicKnockedStateActivable(context, instance, playerView, meta, actionBar, + mapSettingsInfo, reviveHandler, tickFormatter, sidebar, tabList, belowNameTag, stats), + corpse.asKnockActivable(), new Activable() { @Override public void start() { meta.setCorpse(corpse); @@ -200,7 +226,7 @@ public void start() { return new BasicZombiesPlayerState(Component.text("QUIT").color(NamedTextColor.RED), ZombiesPlayerStateKeys.QUIT.key(), List.of(new BasicQuitStateActivable(instance, playerView, mapSettingsInfo, sidebar, tabList, - stateMap, taskScheduler))); + belowNameTag, stateMap, taskScheduler))); }; Map, Function> stateFunctions = @@ -211,10 +237,20 @@ public void start() { PlayerStateSwitcher stateSwitcher = new PlayerStateSwitcher(); + Point location = VecUtils.toPoint(mapSettingsInfo.origin()).add(VecUtils.toPoint(leaderboardInfo.location())); + Hologram hologram = new ViewableHologram(location, leaderboardInfo.gap(), + viewer -> viewer.getUuid().equals(playerView.getUUID())); + hologram.setInstance(instance); + DependencyModule leaderboardModule = + new BestTimeLeaderboard.Module(database, playerView.getUUID(), hologram, mapSettingsInfo, viewProvider, + MiniMessage.miniMessage()); + BestTimeLeaderboard leaderboard = contextManager.makeContext(leaderboardInfo.data()) + .provide(new ModuleDependencyProvider(keyParser, leaderboardModule)); + ZombiesPlayerModule module = - new ZombiesPlayerModule(playerView, meta, coins, kills, equipmentHandler, equipmentCreator, + new ZombiesPlayerModule(playerView, meta, coins, kills, equipmentHandler, equipmentCreator, actionBar, accessRegistry, stateSwitcher, stateFunctions, sidebar, tabList, mapTransactionModifierSource, - playerTransactionModifierSource, flaggable, stats); + playerTransactionModifierSource, flaggable, stats, leaderboard); ZombiesPlayer zombiesPlayer = new BasicZombiesPlayer(scene, module, stateMap, taskScheduler); zombiesPlayerWrapper.set(zombiesPlayer); diff --git a/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayer.java b/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayer.java index dfcf0ec02..a82d0d8a8 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayer.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayer.java @@ -2,12 +2,14 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; -import net.minestom.server.entity.Entity; +import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeModifier; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.instance.Instance; +import net.minestom.server.scoreboard.BelowNameTag; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Activable; import org.phantazm.commons.CancellableState; @@ -19,9 +21,7 @@ import org.phantazm.mob.spawner.MobSpawner; import org.phantazm.zombies.coin.TransactionModifierSource; import org.phantazm.zombies.corpse.CorpseCreator; -import org.phantazm.zombies.map.Door; -import org.phantazm.zombies.map.Flaggable; -import org.phantazm.zombies.map.MapSettingsInfo; +import org.phantazm.zombies.map.*; import org.phantazm.zombies.map.objects.MapObjects; import org.phantazm.zombies.player.state.PlayerStateKey; import org.phantazm.zombies.player.state.ZombiesPlayerState; @@ -49,6 +49,13 @@ public interface ZombiesPlayer extends Activable, Flaggable.Source, Audience { void removeCancellable(@NotNull UUID id); + default void cancellableAttribute(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) { + registerCancellable(CancellableState.named(modifier.getId(), + () -> getPlayer().ifPresent(player -> player.getAttribute(attribute).addModifier(modifier)), + () -> getPlayer().ifPresent(player -> player.getAttribute(attribute).removeModifier(modifier.getId()))), + true); + } + default @NotNull Optional getHeldEquipment() { Optional playerOptional = module().getPlayerView().getPlayer(); if (playerOptional.isEmpty()) { @@ -72,6 +79,11 @@ public interface ZombiesPlayer extends Activable, Flaggable.Source, Audience { @SuppressWarnings("unchecked") default boolean setState(@NotNull PlayerStateKey stateKey, @NotNull TContext context) { + ZombiesPlayerState currentState = module().getStateSwitcher().getState(); + if (currentState != null && currentState.key().equals(stateKey.key())) { + return false; + } + Function stateFunction = (Function)module().getStateFunctions().get(stateKey); if (stateFunction != null) { @@ -126,6 +138,10 @@ default boolean canTakeDamage() { return canDoGenericActions(); } + default boolean canUseEquipment() { + return !isKnocked() && !hasQuit(); + } + default boolean canBeTargeted() { return canDoGenericActions() && getPlayer().map(player -> { GameMode mode = player.getGameMode(); @@ -142,7 +158,7 @@ default boolean canRevive() { return false; } - return getPlayer().filter(value -> value.getPose() == Entity.Pose.SNEAKING).isPresent(); + return getPlayer().filter(Player::isSneaking).isPresent(); } default boolean canTriggerSLA() { @@ -176,10 +192,12 @@ interface Source { @NotNull ZombiesPlayer createPlayer(@NotNull ZombiesScene scene, @NotNull Map zombiesPlayers, - @NotNull MapSettingsInfo mapSettingsInfo, @NotNull Instance instance, @NotNull PlayerView playerView, + @NotNull MapSettingsInfo mapSettingsInfo, @NotNull PlayerCoinsInfo playerCoinsInfo, + @NotNull LeaderboardInfo leaderboardInfo, @NotNull Instance instance, @NotNull PlayerView playerView, @NotNull TransactionModifierSource mapTransactionModifierSource, @NotNull Flaggable flaggable, @NotNull EventNode eventNode, @NotNull Random random, @NotNull MapObjects mapObjects, - @NotNull MobStore mobStore, @NotNull MobSpawner mobSpawner, @NotNull CorpseCreator corpseCreator); + @NotNull MobStore mobStore, @NotNull MobSpawner mobSpawner, @NotNull CorpseCreator corpseCreator, + @NotNull BelowNameTag belowNameTag); } diff --git a/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayerModule.java b/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayerModule.java index 88a708d4c..5e6a235df 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayerModule.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/ZombiesPlayerModule.java @@ -13,7 +13,9 @@ import org.phantazm.zombies.coin.PlayerCoins; import org.phantazm.zombies.coin.TransactionModifierSource; import org.phantazm.zombies.kill.PlayerKills; +import org.phantazm.zombies.leaderboard.BestTimeLeaderboard; import org.phantazm.zombies.map.Flaggable; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import org.phantazm.zombies.player.state.PlayerStateKey; import org.phantazm.zombies.player.state.PlayerStateSwitcher; import org.phantazm.zombies.player.state.ZombiesPlayerState; @@ -32,6 +34,7 @@ public class ZombiesPlayerModule implements DependencyModule { private final PlayerKills kills; private final EquipmentHandler equipmentHandler; private final EquipmentCreator equipmentCreator; + private final ZombiesPlayerActionBar actionBar; private final InventoryAccessRegistry profileSwitcher; private final PlayerStateSwitcher stateSwitcher; private final Map, Function> stateFunctions; @@ -41,22 +44,24 @@ public class ZombiesPlayerModule implements DependencyModule { private final TransactionModifierSource compositeTransactionModifierSource; private final Flaggable flaggable; private final ZombiesPlayerMapStats stats; + private final BestTimeLeaderboard leaderboard; public ZombiesPlayerModule(@NotNull PlayerView playerView, @NotNull ZombiesPlayerMeta meta, @NotNull PlayerCoins coins, @NotNull PlayerKills kills, @NotNull EquipmentHandler equipmentHandler, - @NotNull EquipmentCreator equipmentCreator, @NotNull InventoryAccessRegistry profileSwitcher, - @NotNull PlayerStateSwitcher stateSwitcher, + @NotNull EquipmentCreator equipmentCreator, @NotNull ZombiesPlayerActionBar actionBar, + @NotNull InventoryAccessRegistry profileSwitcher, @NotNull PlayerStateSwitcher stateSwitcher, @NotNull Map, Function> stateFunctions, - @NotNull Sidebar sidebar, - @NotNull TabList tabList, @NotNull TransactionModifierSource mapTransactionModifierSource, + @NotNull Sidebar sidebar, @NotNull TabList tabList, + @NotNull TransactionModifierSource mapTransactionModifierSource, @NotNull TransactionModifierSource playerTransactionModifierSource, @NotNull Flaggable flaggable, - @NotNull ZombiesPlayerMapStats stats) { + @NotNull ZombiesPlayerMapStats stats, @NotNull BestTimeLeaderboard leaderboard) { this.playerView = Objects.requireNonNull(playerView, "playerView"); this.meta = Objects.requireNonNull(meta, "meta"); this.coins = Objects.requireNonNull(coins, "coins"); this.kills = Objects.requireNonNull(kills, "kills"); this.equipmentHandler = Objects.requireNonNull(equipmentHandler, "equipmentHandler"); this.equipmentCreator = Objects.requireNonNull(equipmentCreator, "equipmentCreator"); + this.actionBar = Objects.requireNonNull(actionBar, "actionBar"); this.profileSwitcher = Objects.requireNonNull(profileSwitcher, "profileSwitcher"); this.stateSwitcher = Objects.requireNonNull(stateSwitcher, "stateSwitcher"); this.stateFunctions = Map.copyOf(stateFunctions); @@ -68,6 +73,7 @@ public ZombiesPlayerModule(@NotNull PlayerView playerView, @NotNull ZombiesPlaye TransactionModifierSource.compositeView(mapTransactionModifierSource, playerTransactionModifierSource); this.flaggable = Objects.requireNonNull(flaggable, "flags"); this.stats = Objects.requireNonNull(stats, "stats"); + this.leaderboard = Objects.requireNonNull(leaderboard, "leaderboard"); } public @NotNull ZombiesPlayerMeta getMeta() { @@ -94,6 +100,10 @@ public ZombiesPlayerModule(@NotNull PlayerView playerView, @NotNull ZombiesPlaye return profileSwitcher; } + public @NotNull ZombiesPlayerActionBar getActionBar() { + return actionBar; + } + public @NotNull PlayerStateSwitcher getStateSwitcher() { return stateSwitcher; } @@ -131,4 +141,8 @@ public ZombiesPlayerModule(@NotNull PlayerView playerView, @NotNull ZombiesPlaye public @NotNull ZombiesPlayerMapStats getStats() { return stats; } + + public @NotNull BestTimeLeaderboard getLeaderboard() { + return leaderboard; + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/player/action_bar/ZombiesPlayerActionBar.java b/zombies/src/main/java/org/phantazm/zombies/player/action_bar/ZombiesPlayerActionBar.java new file mode 100644 index 000000000..e8635f024 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/player/action_bar/ZombiesPlayerActionBar.java @@ -0,0 +1,51 @@ +package org.phantazm.zombies.player.action_bar; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Tickable; +import org.phantazm.core.player.PlayerView; + +import java.util.Objects; + +public class ZombiesPlayerActionBar implements Tickable { + + // TODO: configure + public static int GUN_RELOAD_PRIORITY = 1; + + public static int COINS_PRIORITY = 2; + + public static int REVIVE_MESSAGE_PRIORITY = 3; + + public static int REVIVE_MESSAGE_CLEAR_PRIORITY = 4; + + private final PlayerView playerView; + + private Component message = null; + + private int highestPriority = Integer.MIN_VALUE; + + public ZombiesPlayerActionBar(@NotNull PlayerView playerView) { + this.playerView = Objects.requireNonNull(playerView, "playerView"); + } + + @Override + public void tick(long time) { + if (message != null) { + playerView.getPlayer().ifPresent(player -> { + player.sendActionBar(message); + }); + } + + message = null; + highestPriority = Integer.MIN_VALUE; + } + + public void sendActionBar(@NotNull Component component, int priority) { + Objects.requireNonNull(component, "component"); + if (priority > highestPriority) { + message = component; + highestPriority = priority; + } + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicAliveStateActivable.java b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicAliveStateActivable.java index d3b9b11ba..d35903293 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicAliveStateActivable.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicAliveStateActivable.java @@ -1,31 +1,60 @@ package org.phantazm.zombies.player.state; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.MinecraftServer; +import net.minestom.server.adventure.audience.PacketGroupingAudience; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Sidebar; import net.minestom.server.scoreboard.TabList; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Activable; +import org.phantazm.commons.MiniMessageUtils; import org.phantazm.core.inventory.InventoryAccessRegistry; import org.phantazm.core.player.PlayerView; import org.phantazm.zombies.Attributes; +import org.phantazm.zombies.map.MapSettingsInfo; +import org.phantazm.zombies.player.ZombiesPlayerMeta; +import org.phantazm.zombies.player.state.context.AlivePlayerStateContext; -import java.util.Objects; +import java.util.*; public class BasicAliveStateActivable implements Activable { + private static final TagResolver[] EMPTY_TAG_RESOLVER_ARRAY = new TagResolver[0]; + private final AlivePlayerStateContext context; + private final Instance instance; private final InventoryAccessRegistry accessRegistry; private final PlayerView playerView; + private final ZombiesPlayerMeta meta; + private final MapSettingsInfo settings; private final Sidebar sidebar; private final TabList tabList; + private final BelowNameTag belowNameTag; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); private long lastHeal; - public BasicAliveStateActivable(@NotNull InventoryAccessRegistry accessRegistry, @NotNull PlayerView playerView, - @NotNull Sidebar sidebar, @NotNull TabList tabList) { + public BasicAliveStateActivable(@NotNull AlivePlayerStateContext context, @NotNull Instance instance, + @NotNull InventoryAccessRegistry accessRegistry, @NotNull PlayerView playerView, + @NotNull ZombiesPlayerMeta meta, @NotNull MapSettingsInfo settings, @NotNull Sidebar sidebar, + @NotNull TabList tabList, @NotNull BelowNameTag belowNameTag) { + this.context = Objects.requireNonNull(context, "context"); + this.instance = Objects.requireNonNull(instance, "instance"); this.accessRegistry = Objects.requireNonNull(accessRegistry, "accessRegistry"); this.playerView = Objects.requireNonNull(playerView, "playerView"); + this.meta = Objects.requireNonNull(meta, "meta"); + this.settings = Objects.requireNonNull(settings, "settings"); this.sidebar = Objects.requireNonNull(sidebar, "sidebar"); this.tabList = Objects.requireNonNull(tabList, "tabList"); + this.belowNameTag = Objects.requireNonNull(belowNameTag, "belowNameTag"); } @Override @@ -37,8 +66,39 @@ public void start() { player.setGameMode(GameMode.ADVENTURE); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); }); + if (context.isRevive()) { + // TODO: theoretical memleaks here + playerView.getDisplayName().thenAccept(displayName -> { + TagResolver[] tagResolvers = getTagResolvers(displayName); + + if (meta.isInGame()) { + playerView.getPlayer().ifPresent(player -> player.sendMessage( + miniMessage.deserialize(settings.reviveMessageToRevivedFormat(), tagResolvers))); + } + + Set players = new HashSet<>(instance.getPlayers()); + playerView.getPlayer().ifPresent(players::remove); + Audience filteredAudience = PacketGroupingAudience.of(players); + + filteredAudience.sendMessage( + miniMessage.deserialize(settings.reviveMessageToOthersFormat(), tagResolvers)); + }); + + Point point = context.reviveLocation(); + if (point != null) { + instance.playSound(settings.reviveSound(), point.x(), point.y(), point.z()); + } + else { + playerView.getPlayer().ifPresent(player -> { + Pos location = player.getPosition(); + instance.playSound(settings.reviveSound(), location.x(), location.y(), location.z()); + }); + } + } + accessRegistry.switchAccess(InventoryKeys.ALIVE_ACCESS); } @@ -49,6 +109,8 @@ public void tick(long time) { player.setHealth(player.getHealth() + 1F); lastHeal = time; } + + belowNameTag.updateScore(player, (int)Math.floor(player.getHealth())); }); } @@ -62,8 +124,20 @@ public void end() { player.clearEffects(); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); }); + } - accessRegistry.switchAccess(null); + private TagResolver[] getTagResolvers(@NotNull Component displayName) { + boolean reviverPresent = context.reviverName() != null; + List tagResolvers = new ArrayList<>(); + tagResolvers.add(Placeholder.component("revived", displayName)); + tagResolvers.add(MiniMessageUtils.optional("reviver_present", reviverPresent)); + if (reviverPresent) { + tagResolvers.add(Placeholder.component("reviver", context.reviverName())); + } + + return tagResolvers.toArray(EMPTY_TAG_RESOLVER_ARRAY); } + } diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicDeadStateActivable.java b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicDeadStateActivable.java index 6cbe1eaef..f3dd02104 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicDeadStateActivable.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicDeadStateActivable.java @@ -1,24 +1,27 @@ package org.phantazm.zombies.player.state; import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; -import net.kyori.adventure.title.TitlePart; import net.minestom.server.adventure.audience.PacketGroupingAudience; +import net.minestom.server.coordinate.Point; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; +import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Sidebar; import net.minestom.server.scoreboard.TabList; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Activable; import org.phantazm.commons.MiniMessageUtils; -import org.phantazm.core.inventory.InventoryAccessRegistry; +import org.phantazm.core.inventory.*; import org.phantazm.core.player.PlayerView; import org.phantazm.stats.zombies.ZombiesPlayerMapStats; import org.phantazm.zombies.map.MapSettingsInfo; +import org.phantazm.zombies.player.ZombiesPlayerMeta; import org.phantazm.zombies.player.state.context.DeadPlayerStateContext; import java.util.*; @@ -29,23 +32,27 @@ public class BasicDeadStateActivable implements Activable { private final DeadPlayerStateContext context; private final Instance instance; private final PlayerView playerView; + private final ZombiesPlayerMeta meta; private final MapSettingsInfo settings; private final Sidebar sidebar; private final TabList tabList; + private final BelowNameTag belowNameTag; private final ZombiesPlayerMapStats stats; private final MiniMessage miniMessage = MiniMessage.miniMessage(); public BasicDeadStateActivable(@NotNull InventoryAccessRegistry accessRegistry, @NotNull DeadPlayerStateContext context, @NotNull Instance instance, @NotNull PlayerView playerView, - @NotNull MapSettingsInfo settings, @NotNull Sidebar sidebar, @NotNull TabList tabList, - @NotNull ZombiesPlayerMapStats stats) { + @NotNull ZombiesPlayerMeta meta, @NotNull MapSettingsInfo settings, @NotNull Sidebar sidebar, + @NotNull TabList tabList, @NotNull BelowNameTag belowNameTag, @NotNull ZombiesPlayerMapStats stats) { this.accessRegistry = Objects.requireNonNull(accessRegistry, "accessRegistry"); this.context = Objects.requireNonNull(context, "context"); this.instance = Objects.requireNonNull(instance, "instance"); this.playerView = Objects.requireNonNull(playerView, "playerView"); + this.meta = Objects.requireNonNull(meta, "meta"); this.settings = Objects.requireNonNull(settings, "settings"); this.sidebar = Objects.requireNonNull(sidebar, "sidebar"); this.tabList = Objects.requireNonNull(tabList, "tabList"); + this.belowNameTag = Objects.requireNonNull(belowNameTag, "belowNameTag"); this.stats = Objects.requireNonNull(stats, "stats"); } @@ -56,7 +63,10 @@ public void start() { player.setGameMode(GameMode.SPECTATOR); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); }); + + playerView.getDisplayName().thenAccept(displayName -> { if (context.isRejoin()) { TagResolver rejoinerPlaceholder = Placeholder.component("rejoiner", displayName); @@ -64,16 +74,45 @@ public void start() { return; } - Set players = new HashSet<>(instance.getPlayers()); TagResolver[] tagResolvers = getTagResolvers(displayName); - playerView.getPlayer().ifPresent(player -> { - players.remove(player); - player.sendMessage(miniMessage.deserialize(settings.deathMessageToKilledFormat(), tagResolvers)); - }); + if (meta.isInGame()) { + playerView.getPlayer().ifPresent(player -> { + player.sendMessage(miniMessage.deserialize(settings.deathMessageToKilledFormat(), tagResolvers)); + }); + } + + Set players = new HashSet<>(instance.getPlayers()); + playerView.getPlayer().ifPresent(players::remove); Audience instanceAudience = PacketGroupingAudience.of(players); instanceAudience.sendMessage(miniMessage.deserialize(settings.deathMessageToOthersFormat(), tagResolvers)); }); + Point deathLocation = context.getDeathLocation(); + if (deathLocation != null) { + instance.playSound(settings.deathSound(), deathLocation.x(), deathLocation.y(), deathLocation.z()); + } + + if (!context.isRejoin()) { + InventoryAccess aliveAccess = accessRegistry.getAccess(InventoryKeys.ALIVE_ACCESS); + + for (Key key : settings.lostOnDeath()) { + InventoryObjectGroup group = aliveAccess.groups().get(key); + if (group == null) { + continue; + } + + InventoryObject defaultObject = group.defaultObject(); + for (int slot : group.getSlots()) { + if (defaultObject == null) { + accessRegistry.removeObject(aliveAccess, slot); + } + else { + accessRegistry.replaceObject(aliveAccess, slot, defaultObject); + } + } + } + } + stats.setDeaths(stats.getDeaths() + 1); accessRegistry.switchAccess(InventoryKeys.DEAD_ACCESS); } @@ -85,6 +124,7 @@ public void end() { player.setGameMode(GameMode.ADVENTURE); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); }); accessRegistry.switchAccess(null); diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicKnockedStateActivable.java b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicKnockedStateActivable.java index 1bfe9982b..583e5837f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicKnockedStateActivable.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicKnockedStateActivable.java @@ -2,6 +2,7 @@ import com.github.steanky.toolkit.collection.Wrapper; import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; @@ -13,6 +14,7 @@ import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; +import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Sidebar; import net.minestom.server.scoreboard.TabList; import org.jetbrains.annotations.NotNull; @@ -23,6 +25,8 @@ import org.phantazm.stats.zombies.ZombiesPlayerMapStats; import org.phantazm.zombies.map.MapSettingsInfo; import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.player.ZombiesPlayerMeta; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import org.phantazm.zombies.player.state.context.KnockedPlayerStateContext; import org.phantazm.zombies.player.state.revive.ReviveHandler; @@ -39,6 +43,10 @@ public class BasicKnockedStateActivable implements Activable { private final PlayerView playerView; + private final ZombiesPlayerMeta meta; + + private final ZombiesPlayerActionBar actionBar; + private final MapSettingsInfo settings; private final ReviveHandler reviveHandler; @@ -49,22 +57,28 @@ public class BasicKnockedStateActivable implements Activable { private final TabList tabList; + private final BelowNameTag belowNameTag; + private final ZombiesPlayerMapStats stats; private final MiniMessage miniMessage = MiniMessage.miniMessage(); public BasicKnockedStateActivable(@NotNull KnockedPlayerStateContext context, @NotNull Instance instance, - @NotNull PlayerView playerView, @NotNull MapSettingsInfo settings, @NotNull ReviveHandler reviveHandler, + @NotNull PlayerView playerView, @NotNull ZombiesPlayerMeta meta, @NotNull ZombiesPlayerActionBar actionBar, + @NotNull MapSettingsInfo settings, @NotNull ReviveHandler reviveHandler, @NotNull TickFormatter tickFormatter, @NotNull Sidebar sidebar, @NotNull TabList tabList, - @NotNull ZombiesPlayerMapStats stats) { + @NotNull BelowNameTag belowNameTag, @NotNull ZombiesPlayerMapStats stats) { this.context = Objects.requireNonNull(context, "context"); this.instance = Objects.requireNonNull(instance, "instance"); this.playerView = Objects.requireNonNull(playerView, "playerView"); + this.meta = Objects.requireNonNull(meta, "meta"); + this.actionBar = Objects.requireNonNull(actionBar, "actionBar"); this.settings = Objects.requireNonNull(settings, "settings"); this.reviveHandler = Objects.requireNonNull(reviveHandler, "reviveHandler"); this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); this.sidebar = Objects.requireNonNull(sidebar, "sidebar"); this.tabList = Objects.requireNonNull(tabList, "tabList"); + this.belowNameTag = Objects.requireNonNull(belowNameTag, "belowNameTag"); this.stats = Objects.requireNonNull(stats, "stats"); } @@ -79,24 +93,29 @@ public void start() { player.setGameMode(GameMode.SPECTATOR); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); context.getVehicle().addPassenger(player); }); playerView.getDisplayName().thenAccept(displayName -> { - Set players = new HashSet<>(instance.getPlayers()); TagResolver[] tagResolvers = getTagResolvers(displayName); - playerView.getPlayer().ifPresent(player -> { - players.remove(player); - player.sendMessage(miniMessage.deserialize(settings.knockedMessageToKnockedFormat(), tagResolvers)); - }); - Audience instanceAudience = PacketGroupingAudience.of(players); - instanceAudience.sendMessage( + if (meta.isInGame()) { + playerView.getPlayer().ifPresent(player -> { + player.sendMessage(miniMessage.deserialize(settings.knockedMessageToKnockedFormat(), tagResolvers)); + }); + } + + Set players = new HashSet<>(instance.getPlayers()); + playerView.getPlayer().ifPresent(players::remove); + Audience filteredAudience = PacketGroupingAudience.of(players); + filteredAudience.sendMessage( miniMessage.deserialize(settings.knockedMessageToOthersFormat(), tagResolvers)); - instanceAudience.sendTitlePart(TitlePart.TITLE, + filteredAudience.sendTitlePart(TitlePart.TITLE, miniMessage.deserialize(settings.knockedTitleFormat(), tagResolvers)); - instanceAudience.sendTitlePart(TitlePart.SUBTITLE, + filteredAudience.sendTitlePart(TitlePart.SUBTITLE, miniMessage.deserialize(settings.knockedSubtitleFormat(), tagResolvers)); }); + instance.playSound(settings.knockedSound(), Sound.Emitter.self()); stats.setKnocks(stats.getKnocks() + 1); } @@ -127,12 +146,13 @@ public void end() { player.setFlyingSpeed(0.4F); player.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.1F); player.setGameMode(GameMode.ADVENTURE); - player.sendActionBar(Component.empty()); sidebar.addViewer(player); tabList.addViewer(player); + belowNameTag.addViewer(player); context.getVehicle().remove(); player.teleport(Pos.fromPoint(context.getKnockLocation())); }); + actionBar.sendActionBar(Component.empty(), ZombiesPlayerActionBar.REVIVE_MESSAGE_CLEAR_PRIORITY); } private void sendReviveStatus(@NotNull ZombiesPlayer reviver, @NotNull Component knockedDisplayName, @@ -143,7 +163,7 @@ private void sendReviveStatus(@NotNull ZombiesPlayer reviver, @NotNull Component Placeholder.unparsed("time", tickFormatter.format(reviveHandler.getTicksUntilRevive())); Component message = miniMessage.deserialize(settings.reviveStatusToReviverFormat(), knockedNamePlaceholder, timePlaceholder); - reviverPlayer.sendActionBar(message); + reviver.module().getActionBar().sendActionBar(message, ZombiesPlayerActionBar.REVIVE_MESSAGE_PRIORITY); }); playerView.getPlayer().ifPresent(player -> { TagResolver reviverNamePlaceholder = Placeholder.component("reviver", reviverDisplayName); @@ -151,7 +171,7 @@ private void sendReviveStatus(@NotNull ZombiesPlayer reviver, @NotNull Component Placeholder.unparsed("time", tickFormatter.format(reviveHandler.getTicksUntilRevive())); Component message = miniMessage.deserialize(settings.reviveStatusToKnockedFormat(), reviverNamePlaceholder, timePlaceholder); - player.sendActionBar(message); + actionBar.sendActionBar(message, ZombiesPlayerActionBar.REVIVE_MESSAGE_PRIORITY); }); } @@ -159,7 +179,8 @@ private void sendDyingStatus() { playerView.getPlayer().ifPresent(player -> { TagResolver timePlaceholder = Placeholder.component("time", Component.text(tickFormatter.format(Math.max(reviveHandler.getTicksUntilDeath(), 0)))); - player.sendActionBar(miniMessage.deserialize(settings.dyingStatusFormat(), timePlaceholder)); + actionBar.sendActionBar(miniMessage.deserialize(settings.dyingStatusFormat(), timePlaceholder), + ZombiesPlayerActionBar.REVIVE_MESSAGE_PRIORITY); }); } diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicQuitStateActivable.java b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicQuitStateActivable.java index d21b647ab..4d4427168 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/BasicQuitStateActivable.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/BasicQuitStateActivable.java @@ -1,10 +1,12 @@ package org.phantazm.zombies.player.state; +import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.instance.Instance; +import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Sidebar; import net.minestom.server.scoreboard.TabList; import org.jetbrains.annotations.NotNull; @@ -24,18 +26,21 @@ public class BasicQuitStateActivable implements Activable { private final MapSettingsInfo settings; private final Sidebar sidebar; private final TabList tabList; + private final BelowNameTag belowNameTag; private final Map stateMap; private final TickTaskScheduler scheduler; private final MiniMessage miniMessage = MiniMessage.miniMessage(); public BasicQuitStateActivable(@NotNull Instance instance, @NotNull PlayerView playerView, @NotNull MapSettingsInfo settings, @NotNull Sidebar sidebar, @NotNull TabList tabList, + @NotNull BelowNameTag belowNameTag, @NotNull Map stateMap, @NotNull TickTaskScheduler scheduler) { this.instance = Objects.requireNonNull(instance, "instance"); this.playerView = Objects.requireNonNull(playerView, "playerView"); this.settings = Objects.requireNonNull(settings, "settings"); this.sidebar = Objects.requireNonNull(sidebar, "sidebar"); this.tabList = Objects.requireNonNull(tabList, "tabList"); + this.belowNameTag = Objects.requireNonNull(belowNameTag, "belowNameTag"); this.stateMap = Objects.requireNonNull(stateMap, "stateMap"); this.scheduler = Objects.requireNonNull(scheduler, "scheduler"); } @@ -48,7 +53,11 @@ public void start() { player.setExp(0); sidebar.removeViewer(player); tabList.removeViewer(player); + belowNameTag.removeViewer(player); player.setHealth(player.getMaxHealth()); + player.resetTitle(); + player.sendActionBar(Component.empty()); + player.stopSound(SoundStop.all()); }); playerView.getDisplayName().thenAccept(displayName -> { TagResolver quitterPlaceholder = Placeholder.component("quitter", displayName); diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/PlayerStateSwitcher.java b/zombies/src/main/java/org/phantazm/zombies/player/state/PlayerStateSwitcher.java index 71d1e5281..d329bdb57 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/PlayerStateSwitcher.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/PlayerStateSwitcher.java @@ -37,7 +37,7 @@ public void end() { state.end(); } - public @NotNull ZombiesPlayerState getState() { + public ZombiesPlayerState getState() { return state; } diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/ZombiesPlayerStateKeys.java b/zombies/src/main/java/org/phantazm/zombies/player/state/ZombiesPlayerStateKeys.java index 00882bf7c..58d7fb7fb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/ZombiesPlayerStateKeys.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/ZombiesPlayerStateKeys.java @@ -2,14 +2,11 @@ import net.kyori.adventure.key.Key; import org.phantazm.commons.Namespaces; -import org.phantazm.zombies.player.state.context.DeadPlayerStateContext; -import org.phantazm.zombies.player.state.context.QuitPlayerStateContext; -import org.phantazm.zombies.player.state.context.KnockedPlayerStateContext; -import org.phantazm.zombies.player.state.context.NoContext; +import org.phantazm.zombies.player.state.context.*; public class ZombiesPlayerStateKeys { - public static final PlayerStateKey ALIVE = + public static final PlayerStateKey ALIVE = new PlayerStateKey<>(Key.key(Namespaces.PHANTAZM, "zombies.player.state.alive")); public static final PlayerStateKey KNOCKED = diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/context/AlivePlayerStateContext.java b/zombies/src/main/java/org/phantazm/zombies/player/state/context/AlivePlayerStateContext.java new file mode 100644 index 000000000..8b319cda0 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/context/AlivePlayerStateContext.java @@ -0,0 +1,19 @@ +package org.phantazm.zombies.player.state.context; + +import net.kyori.adventure.text.Component; +import net.minestom.server.coordinate.Point; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record AlivePlayerStateContext(@Nullable Component reviverName, + @Nullable Point reviveLocation, boolean isRevive) { + + public static @NotNull AlivePlayerStateContext regular() { + return new AlivePlayerStateContext(null, null, false); + } + + public static @NotNull AlivePlayerStateContext revive(@Nullable Component reviverName, @Nullable Point reviveLocation) { + return new AlivePlayerStateContext(reviverName, reviveLocation, true); + } + +} diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/context/DeadPlayerStateContext.java b/zombies/src/main/java/org/phantazm/zombies/player/state/context/DeadPlayerStateContext.java index b58a61283..0952ea4f8 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/context/DeadPlayerStateContext.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/context/DeadPlayerStateContext.java @@ -1,6 +1,7 @@ package org.phantazm.zombies.player.state.context; import net.kyori.adventure.text.Component; +import net.minestom.server.coordinate.Point; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -8,24 +9,33 @@ public class DeadPlayerStateContext { + private final Point deathLocation; + private final Component killer; private final Component deathRoomName; private final boolean isRejoin; - protected DeadPlayerStateContext(@Nullable Component killer, @Nullable Component deathRoomName, boolean isRejoin) { + protected DeadPlayerStateContext(@Nullable Point deathLocation, @Nullable Component killer, + @Nullable Component deathRoomName, boolean isRejoin) { + this.deathLocation = deathLocation; this.killer = killer; this.deathRoomName = deathRoomName; this.isRejoin = isRejoin; } - public static DeadPlayerStateContext killed(@Nullable Component killer, @Nullable Component deathRoomName) { - return new DeadPlayerStateContext(killer, deathRoomName, false); + public static DeadPlayerStateContext killed(@Nullable Point knockLocation, @Nullable Component killer, + @Nullable Component deathRoomName) { + return new DeadPlayerStateContext(knockLocation, killer, deathRoomName, false); } public static DeadPlayerStateContext rejoin() { - return new DeadPlayerStateContext(null, null, true); + return new DeadPlayerStateContext(null, null, null, true); + } + + public @Nullable Point getDeathLocation() { + return deathLocation; } public @NotNull Optional getKiller() { diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverFinder.java b/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverFinder.java deleted file mode 100644 index ea7e9b8ca..000000000 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverFinder.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.phantazm.zombies.player.state.revive; - -import com.github.steanky.toolkit.collection.Wrapper; -import net.minestom.server.coordinate.Point; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.Player; -import net.minestom.server.instance.EntityTracker; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.NotNull; -import org.phantazm.core.player.PlayerView; -import org.phantazm.zombies.player.ZombiesPlayer; -import org.phantazm.zombies.player.ZombiesPlayerMeta; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Supplier; - -public class NearbyReviverFinder implements Supplier { - - private final Map zombiesPlayers; - - private final PlayerView playerView; - - private final double reviveRadius; - - public NearbyReviverFinder(@NotNull Map zombiesPlayers, - @NotNull PlayerView playerView, double reviveRadius) { - this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); - this.playerView = Objects.requireNonNull(playerView, "playerView"); - this.reviveRadius = reviveRadius; - } - - @Override - public ZombiesPlayer get() { - Optional knockedPlayerOptional = playerView.getPlayer(); - if (knockedPlayerOptional.isEmpty()) { - return null; - } - Player knockedPlayer = knockedPlayerOptional.get(); - Point knockedPosition = knockedPlayer.getPosition(); - - Instance instance = knockedPlayer.getInstance(); - if (instance == null) { - return null; - } - - Wrapper playerWrapper = Wrapper.ofNull(); - instance.getEntityTracker() - .nearbyEntitiesUntil(knockedPosition, reviveRadius, EntityTracker.Target.PLAYERS, candidate -> { - UUID candidateUUID = candidate.getUuid(); - ZombiesPlayer revivingPlayer = zombiesPlayers.get(candidateUUID); - if (revivingPlayer == null || candidateUUID.equals(playerView.getUUID()) || !revivingPlayer.canRevive()) { - return false; - } - - ZombiesPlayerMeta meta = revivingPlayer.module().getMeta(); - if (meta.isReviving()) { - return false; - } - - Optional reviverPlayerOptional = revivingPlayer.getPlayer(); - if (reviverPlayerOptional.isEmpty()) { - return false; - } - Player player = reviverPlayerOptional.get(); - - if (player.getPosition().distance(knockedPosition) <= reviveRadius) { - playerWrapper.set(revivingPlayer); - return true; - } - - return false; - }); - - return playerWrapper.get(); - } -} diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverPredicate.java b/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverPredicate.java new file mode 100644 index 000000000..503ab1e12 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/revive/NearbyReviverPredicate.java @@ -0,0 +1,51 @@ +package org.phantazm.zombies.player.state.revive; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.player.PlayerView; +import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.player.ZombiesPlayerMeta; + +import java.util.*; +import java.util.function.Predicate; + +public class NearbyReviverPredicate implements Predicate { + + private final PlayerView playerView; + + private final double reviveRadius; + + public NearbyReviverPredicate(@NotNull PlayerView playerView, double reviveRadius) { + this.playerView = Objects.requireNonNull(playerView, "playerView"); + this.reviveRadius = reviveRadius; + } + + @Override + public boolean test(ZombiesPlayer revivingPlayer) { + Optional knockedPlayerOptional = playerView.getPlayer(); + if (knockedPlayerOptional.isEmpty()) { + return false; + } + Player knockedPlayer = knockedPlayerOptional.get(); + Point knockedPosition = knockedPlayer.getPosition(); + + Instance instance = knockedPlayer.getInstance(); + if (instance == null) { + return false; + } + + if (revivingPlayer.getUUID().equals(playerView.getUUID()) || !revivingPlayer.canRevive()) { + return false; + } + + Optional reviverPlayerOptional = revivingPlayer.getPlayer(); + if (reviverPlayerOptional.isEmpty()) { + return false; + } + Player player = reviverPlayerOptional.get(); + + return player.getPosition().distance(knockedPosition) <= reviveRadius; + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/player/state/revive/ReviveHandler.java b/zombies/src/main/java/org/phantazm/zombies/player/state/revive/ReviveHandler.java index 188e2b91f..94dce2540 100644 --- a/zombies/src/main/java/org/phantazm/zombies/player/state/revive/ReviveHandler.java +++ b/zombies/src/main/java/org/phantazm/zombies/player/state/revive/ReviveHandler.java @@ -5,16 +5,24 @@ import org.jetbrains.annotations.Nullable; import org.phantazm.commons.Activable; import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import org.phantazm.zombies.player.state.ZombiesPlayerState; +import org.phantazm.zombies.player.state.context.AlivePlayerStateContext; +import org.phantazm.zombies.player.state.context.KnockedPlayerStateContext; +import java.util.Collection; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; public class ReviveHandler implements Activable { - private final Supplier defaultStateSupplier; + private final KnockedPlayerStateContext context; + private final Collection zombiesPlayers; + private final Function defaultStateCreator; private final Supplier deathStateSupplier; - private final Supplier reviverFinder; + private final Predicate reviverPredicate; private final long deathTime; @@ -25,16 +33,24 @@ public class ReviveHandler implements Activable { private long ticksUntilRevive = -1; - public ReviveHandler(@NotNull Supplier defaultStateSupplier, + public ReviveHandler(@NotNull KnockedPlayerStateContext context, + @NotNull Collection zombiesPlayers, + @NotNull Function defaultStateCreator, @NotNull Supplier deathStateSupplier, - @NotNull Supplier reviverFinder, long deathTime) { - this.defaultStateSupplier = Objects.requireNonNull(defaultStateSupplier, "defaultStateSupplier"); + @NotNull Predicate reviverPredicate, long deathTime) { + this.context = Objects.requireNonNull(context, "context"); + this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); + this.defaultStateCreator = Objects.requireNonNull(defaultStateCreator, "defaultStateCreator"); this.deathStateSupplier = Objects.requireNonNull(deathStateSupplier, "deathStateSupplier"); - this.reviverFinder = Objects.requireNonNull(reviverFinder, "reviverFinder"); + this.reviverPredicate = Objects.requireNonNull(reviverPredicate, "reviverPredicate"); this.deathTime = deathTime; this.ticksUntilDeath = deathTime; } + public @NotNull KnockedPlayerStateContext context() { + return context; + } + public @NotNull Optional getSuggestedState() { if (cachedDeathState != null) { return Optional.of(cachedDeathState); @@ -62,7 +78,12 @@ public void tick(long time) { } if (ticksUntilRevive == 0) { if (cachedDefaultState == null) { - cachedDefaultState = defaultStateSupplier.get(); + Component reviverName = null; + if (reviver != null) { + reviverName = reviver.module().getPlayerView().getDisplayNameIfCached().orElse(null); + } + cachedDefaultState = defaultStateCreator.apply( + AlivePlayerStateContext.revive(reviverName, context.getKnockLocation())); } if (reviver != null) { @@ -75,7 +96,12 @@ public void tick(long time) { } if (reviver == null) { - reviver = reviverFinder.get(); + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (!zombiesPlayer.module().getMeta().isReviving() && reviverPredicate.test(zombiesPlayer)) { + reviver = zombiesPlayer; + break; + } + } if (reviver != null) { ticksUntilDeath = deathTime; reviver.module().getMeta().setReviving(true); @@ -85,7 +111,7 @@ public void tick(long time) { --ticksUntilDeath; } } - else if (!reviver.canRevive()) { + else if (!reviverPredicate.test(reviver)) { clearReviverState(); reviver = null; ticksUntilRevive = -1; @@ -125,7 +151,8 @@ public void setReviver(@Nullable ZombiesPlayer reviver) { private void clearReviverState() { if (reviver != null) { reviver.module().getMeta().setReviving(false); - reviver.getPlayer().ifPresent(player -> player.sendActionBar(Component.empty())); + reviver.module().getActionBar() + .sendActionBar(Component.empty(), ZombiesPlayerActionBar.REVIVE_MESSAGE_CLEAR_PRIORITY); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/BossBarTimerAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/BossBarTimerAction.java index bc53e1b92..8f0dce234 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/BossBarTimerAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/BossBarTimerAction.java @@ -3,16 +3,21 @@ import com.github.steanky.element.core.annotation.*; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.MinecraftServer; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.CancellableState; import org.phantazm.commons.MathUtils; +import org.phantazm.core.time.TickFormatter; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.powerup.Powerup; import org.phantazm.zombies.powerup.predicate.DeactivationPredicate; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.function.Supplier; @@ -22,18 +27,21 @@ public class BossBarTimerAction implements Supplier { private final Data data; private final Instance instance; private final Map playerMap; + private final TickFormatter tickFormatter; @FactoryMethod public BossBarTimerAction(@NotNull Data data, @NotNull Instance instance, - @NotNull Map playerMap) { + @NotNull Map playerMap, + @NotNull @Child("tick_formatter") TickFormatter tickFormatter) { this.data = data; this.instance = instance; this.playerMap = playerMap; + this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); } @Override public PowerupAction get() { - return new Action(data, instance, playerMap); + return new Action(data, instance, playerMap, tickFormatter); } private static class Action implements PowerupAction { @@ -41,12 +49,14 @@ private static class Action implements PowerupAction { private final Instance instance; private final DeactivationPredicate predicate; private final Map playerMap; + private final TickFormatter tickFormatter; private final UUID id; private long startTime = -1; private BossBar bossBar; - private Action(Data data, Instance instance, Map playerMap) { + private Action(Data data, Instance instance, Map playerMap, + TickFormatter tickFormatter) { this.data = data; this.instance = instance; this.predicate = new DeactivationPredicate() { @@ -61,6 +71,7 @@ public boolean shouldDeactivate(long time) { } }; this.playerMap = playerMap; + this.tickFormatter = tickFormatter; this.id = UUID.randomUUID(); } @@ -72,6 +83,7 @@ public void tick(long time) { } long elapsedTime = (time - startTime) / MinecraftServer.TICK_MS; + bossBar.name(createBossBarName(time)); bossBar.progress((float)MathUtils.clamp(1D - ((float)elapsedTime / (float)data.duration), 0, 1)); } @@ -79,7 +91,7 @@ public void tick(long time) { public void activate(@NotNull Powerup powerup, @NotNull ZombiesPlayer player, long time) { this.startTime = System.currentTimeMillis(); - BossBar bossBar = BossBar.bossBar(data.name, 1.0F, data.color, data.overlay); + BossBar bossBar = BossBar.bossBar(createBossBarName(time), 1.0F, data.color, data.overlay); instance.showBossBar(bossBar); this.bossBar = bossBar; @@ -90,17 +102,27 @@ public void activate(@NotNull Powerup powerup, @NotNull ZombiesPlayer player, lo } } + private Component createBossBarName(long time) { + long delta = (time - startTime) / MinecraftServer.TICK_MS; + long remainingTicks = data.duration - delta; + + TagResolver timePlaceholder = Placeholder.unparsed("time", tickFormatter.format(remainingTicks)); + return MiniMessage.miniMessage().deserialize(data.format, timePlaceholder); + } + @Override public void deactivate(@NotNull ZombiesPlayer player) { BossBar bossBar = this.bossBar; - if (bossBar != null) { - for (ZombiesPlayer zombiesPlayer : playerMap.values()) { - zombiesPlayer.removeCancellable(id); - } + if (bossBar == null) { + return; + } - MinecraftServer.getBossBarManager().destroyBossBar(bossBar); - this.bossBar = null; + for (ZombiesPlayer zombiesPlayer : playerMap.values()) { + zombiesPlayer.removeCancellable(id); } + + MinecraftServer.getBossBarManager().destroyBossBar(bossBar); + this.bossBar = null; } @Override @@ -111,8 +133,9 @@ public void deactivate(@NotNull ZombiesPlayer player) { @DataObject public record Data(long duration, - @NotNull Component name, + @NotNull String format, @NotNull BossBar.Color color, - @NotNull BossBar.Overlay overlay) { + @NotNull BossBar.Overlay overlay, + @NotNull @ChildPath("tick_formatter") String tickFormatter) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/KillAllInRadiusAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/KillAllInRadiusAction.java index b49d1128a..8b43e7a6d 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/KillAllInRadiusAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/KillAllInRadiusAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.powerup.action; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -20,6 +21,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.kill_all_in_radius") +@Cache(false) public class KillAllInRadiusAction implements Supplier { private final Data data; private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapFlaggingAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapFlaggingAction.java index af4b2988f..bcf64b6c4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapFlaggingAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapFlaggingAction.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.map_flagging") +@Cache(false) public class MapFlaggingAction implements Supplier { private final Data data; private final Supplier deactivationPredicate; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierAddAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierAddAction.java index a896807c8..93c16be1e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierAddAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierAddAction.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.map_transaction_modifier.add") +@Cache(false) public class MapTransactionModifierAddAction implements Supplier { private final Data data; private final TransactionModifierSource transactionModifierSource; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierMultiplyAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierMultiplyAction.java index eaddd7df7..887a2a535 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierMultiplyAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/MapTransactionModifierMultiplyAction.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.map_transaction_modifier.multiply") +@Cache(false) public class MapTransactionModifierMultiplyAction implements Supplier { private final Data data; private final TransactionModifierSource transactionModifierSource; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/ModifyWindowsAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/ModifyWindowsAction.java index 113bd1a92..f4f2ef8c4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/ModifyWindowsAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/ModifyWindowsAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.powerup.action; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -12,6 +13,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.modify_windows") +@Cache(false) public class ModifyWindowsAction implements Supplier { private final Data data; private final Supplier windowHandler; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlaySoundAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlaySoundAction.java index c98a540f3..8d8c672e8 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlaySoundAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlaySoundAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.powerup.action; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -14,6 +15,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.play_sound") +@Cache(false) public class PlaySoundAction implements Supplier { private final Data data; private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlayerFlaggingAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlayerFlaggingAction.java index e977d69ea..18afc88fd 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlayerFlaggingAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/PlayerFlaggingAction.java @@ -11,6 +11,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.player_flagging") +@Cache(false) public class PlayerFlaggingAction implements Supplier { private final Data data; private final Supplier deactivationPredicate; diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/RefillAmmoAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/RefillAmmoAction.java new file mode 100644 index 000000000..48c6cb8f1 --- /dev/null +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/RefillAmmoAction.java @@ -0,0 +1,55 @@ +package org.phantazm.zombies.powerup.action; + +import com.github.steanky.element.core.annotation.Cache; +import com.github.steanky.element.core.annotation.FactoryMethod; +import com.github.steanky.element.core.annotation.Model; +import org.jetbrains.annotations.NotNull; +import org.phantazm.core.inventory.InventoryObject; +import org.phantazm.zombies.equipment.gun.Gun; +import org.phantazm.zombies.player.ZombiesPlayer; +import org.phantazm.zombies.powerup.Powerup; + +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +@Model("zombies.powerup.action.refill_ammo") +@Cache(false) +public class RefillAmmoAction implements Supplier { + private final Map zombiesPlayers; + + @FactoryMethod + public RefillAmmoAction(@NotNull Map zombiesPlayers) { + this.zombiesPlayers = zombiesPlayers; + } + + @Override + public PowerupAction get() { + return new Action(zombiesPlayers); + } + + private static class Action extends InstantAction { + private final Map zombiesPlayers; + + private Action(Map zombiesPlayers) { + this.zombiesPlayers = zombiesPlayers; + } + + @Override + public void activate(@NotNull Powerup powerup, @NotNull ZombiesPlayer player, long time) { + for (ZombiesPlayer zombiesPlayer : zombiesPlayers.values()) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + zombiesPlayer.module().getEquipmentHandler().accessRegistry().getCurrentAccess().ifPresent(access -> { + for (InventoryObject object : access.profile().objects()) { + if (object instanceof Gun gun) { + gun.refill(); + } + } + }); + } + } + } +} diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendMessageAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendMessageAction.java index 03d4e82a8..19acc9143 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendMessageAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendMessageAction.java @@ -1,20 +1,23 @@ package org.phantazm.zombies.powerup.action; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.powerup.Powerup; -import java.util.IllegalFormatException; import java.util.Objects; import java.util.function.Supplier; @Model("zombies.powerup.action.send_message") +@Cache(false) public class SendMessageAction implements Supplier { private final Data data; private final Instance instance; @@ -31,7 +34,7 @@ public PowerupAction get() { } @DataObject - public record Data(@NotNull String message, boolean broadcast) { + public record Data(@NotNull String format, boolean broadcast) { } private static class Action extends InstantAction { @@ -55,16 +58,10 @@ public void activate(@NotNull Powerup powerup, @NotNull ZombiesPlayer player, lo } private Component getComponent(ZombiesPlayer player) { - Component formattedMessage; - try { - String message = String.format(data.message, player.getUsername()); - formattedMessage = MiniMessage.miniMessage().deserialize(message); - } - catch (IllegalFormatException ignored) { - formattedMessage = MiniMessage.miniMessage().deserialize(data.message); - } + Component playerName = player.module().getPlayerView().getDisplayNameIfPresent(); + TagResolver playerPlaceholder = Placeholder.component("player", playerName); - return formattedMessage; + return MiniMessage.miniMessage().deserialize(data.format, playerPlaceholder); } } } diff --git a/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendTitleAction.java b/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendTitleAction.java index 21c00bf36..6a6f7b283 100644 --- a/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendTitleAction.java +++ b/zombies/src/main/java/org/phantazm/zombies/powerup/action/SendTitleAction.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.powerup.action; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -14,6 +15,7 @@ import java.util.function.Supplier; @Model("zombies.powerup.action.send_title") +@Cache(false) public class SendTitleAction implements Supplier { private final Data data; private final Instance instance; diff --git a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinHelper.java b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinHelper.java index a69cb2bd0..4d166ddd1 100644 --- a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinHelper.java +++ b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinHelper.java @@ -3,15 +3,23 @@ import net.kyori.adventure.key.Key; import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.game.scene.*; +import org.phantazm.core.game.scene.SceneRouter; +import org.phantazm.core.game.scene.SceneTransferHelper; import org.phantazm.core.player.PlayerView; import org.phantazm.core.player.PlayerViewProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; import java.util.function.Function; public class ZombiesJoinHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(ZombiesJoinHelper.class); + private final PlayerViewProvider viewProvider; private final SceneRouter router; @@ -26,27 +34,50 @@ public ZombiesJoinHelper(@NotNull PlayerViewProvider viewProvider, this.transferHelper = Objects.requireNonNull(transferHelper, "transferHelper"); } - public void joinGame(@NotNull Player joiner, @NotNull Collection playerViews, @NotNull Key targetMap) { - joinInternal(joiner, playerViews, joinRequest -> ZombiesRouteRequest.joinGame(targetMap, joinRequest)); + public void joinGame(@NotNull Player joiner, @NotNull Collection playerViews, @NotNull Key targetMap, + boolean isRestricted) { + joinInternal(joiner, playerViews, joinRequest -> ZombiesRouteRequest.joinGame(targetMap, joinRequest), + isRestricted); } public void rejoinGame(@NotNull Player joiner, @NotNull UUID targetGame) { joinInternal(joiner, Collections.singleton(viewProvider.fromPlayer(joiner)), - joinRequest -> ZombiesRouteRequest.rejoinGame(targetGame, joinRequest)); + joinRequest -> ZombiesRouteRequest.rejoinGame(targetGame, joinRequest), false); } private void joinInternal(@NotNull Player joiner, @NotNull Collection playerViews, - @NotNull Function routeRequestCreator) { - ZombiesJoinRequest joinRequest = () -> playerViews; - RouteResult result = router.findScene(routeRequestCreator.apply(joinRequest)); - - if (result.message().isPresent()) { - joiner.sendMessage(result.message().get()); - } - else if (result.scene().isPresent()) { - ZombiesScene scene = result.scene().get(); - transferHelper.transfer(scene, joinRequest, playerViews, viewProvider.fromPlayer(joiner)); - } + @NotNull Function routeRequestCreator, boolean isRestricted) { + UUID uuid = UUID.randomUUID(); + ZombiesJoinRequest joinRequest = new ZombiesJoinRequest() { + @Override + public @NotNull Collection getPlayers() { + return playerViews; + } + + @Override + public @NotNull UUID getUUID() { + return uuid; + } + + @Override + public boolean isRestricted() { + return isRestricted; + } + }; + router.findScene(routeRequestCreator.apply(joinRequest)).whenComplete((result, throwable) -> { + if (throwable != null) { + LOGGER.warn("Exception while finding zombies scene", throwable); + return; + } + + if (result.message().isPresent()) { + joiner.sendMessage(result.message().get()); + } + else if (result.scene().isPresent()) { + ZombiesScene scene = result.scene().get(); + transferHelper.transfer(scene, joinRequest, playerViews, viewProvider.fromPlayer(joiner)); + } + }); } } diff --git a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinRequest.java b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinRequest.java index af6c1fa05..746286a4e 100644 --- a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinRequest.java +++ b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesJoinRequest.java @@ -5,13 +5,16 @@ import org.phantazm.core.player.PlayerView; import java.util.Collection; -import java.util.Set; import java.util.UUID; public interface ZombiesJoinRequest extends SceneJoinRequest { @NotNull Collection getPlayers(); + @NotNull UUID getUUID(); + + boolean isRestricted(); + @Override default int getRequestWeight() { return getPlayers().size(); diff --git a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesScene.java b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesScene.java index 00990ce94..0c8a54520 100644 --- a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesScene.java +++ b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesScene.java @@ -14,22 +14,24 @@ import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; import org.phantazm.commons.TickTaskScheduler; +import org.phantazm.core.VecUtils; import org.phantazm.core.game.scene.InstanceScene; import org.phantazm.core.game.scene.TransferResult; import org.phantazm.core.game.scene.Utils; import org.phantazm.core.game.scene.fallback.SceneFallback; import org.phantazm.core.player.PlayerView; +import org.phantazm.stats.zombies.ZombiesDatabase; import org.phantazm.zombies.map.MapSettingsInfo; import org.phantazm.zombies.map.ZombiesMap; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.state.ZombiesPlayerStateKeys; +import org.phantazm.zombies.player.state.context.AlivePlayerStateContext; import org.phantazm.zombies.player.state.context.DeadPlayerStateContext; -import org.phantazm.zombies.player.state.context.NoContext; import org.phantazm.zombies.stage.Stage; import org.phantazm.zombies.stage.StageTransition; -import org.phantazm.stats.zombies.ZombiesDatabase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +42,6 @@ public class ZombiesScene extends InstanceScene { private static final Logger LOGGER = LoggerFactory.getLogger(ZombiesScene.class); - private static final CompletableFuture[] EMPTY_COMPLETABLE_FUTURE_ARRAY = new CompletableFuture[0]; private final ZombiesMap map; private final Map zombiesPlayers; private final MapSettingsInfo mapSettingsInfo; @@ -50,16 +51,21 @@ public class ZombiesScene extends InstanceScene { private final TickTaskScheduler taskScheduler; private final ZombiesDatabase database; private final EventNode sceneNode; + private final UUID allowedRequestUUID; private boolean joinable = true; + private final Object joinLock = new Object(); + + private volatile int pendingPlayers = 0; + public ZombiesScene(@NotNull UUID uuid, @NotNull ZombiesMap map, @NotNull Map zombiesPlayers, @NotNull Instance instance, @NotNull SceneFallback fallback, @NotNull MapSettingsInfo mapSettingsInfo, @NotNull StageTransition stageTransition, @NotNull LeaveHandler leaveHandler, @NotNull Function playerCreator, @NotNull TickTaskScheduler taskScheduler, @NotNull ZombiesDatabase database, - @NotNull EventNode sceneNode) { - super(uuid, instance, fallback); + @NotNull EventNode sceneNode, @Nullable UUID allowedRequestUUID) { + super(uuid, instance, fallback, VecUtils.toPoint(mapSettingsInfo.spawn())); this.map = Objects.requireNonNull(map, "map"); this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); this.mapSettingsInfo = Objects.requireNonNull(mapSettingsInfo, "mapSettingsInfo"); @@ -69,6 +75,7 @@ public ZombiesScene(@NotNull UUID uuid, @NotNull ZombiesMap map, @NotNull Map getSceneNode() { @@ -101,105 +108,130 @@ public boolean isComplete() { @Override public @NotNull TransferResult join(@NotNull ZombiesJoinRequest joinRequest) { - if (isShutdown()) { - return TransferResult.failure(Component.text("Game is shutdown.")); - } - if (!isJoinable()) { - return TransferResult.failure(Component.text("Game is not joinable.")); - } + synchronized (joinLock) { + if (isShutdown()) { + return TransferResult.failure(Component.text("Game is shutdown.")); + } + if (!isJoinable()) { + return TransferResult.failure(Component.text("Game is not joinable.")); + } + if (false && allowedRequestUUID != null && !joinRequest.getUUID().equals(allowedRequestUUID)) { + return TransferResult.failure(Component.text("You aren't allowed to join this game.")); + } - Collection oldPlayers = new ArrayList<>(joinRequest.getPlayers().size()); - Collection newPlayers = new ArrayList<>(joinRequest.getPlayers().size()); - for (PlayerView player : joinRequest.getPlayers()) { - ZombiesPlayer zombiesPlayer = zombiesPlayers.get(player.getUUID()); - if (zombiesPlayer != null) { - if (zombiesPlayer.hasQuit()) { - oldPlayers.add(zombiesPlayer); + Collection oldPlayers = new ArrayList<>(joinRequest.getPlayers().size()); + Collection newPlayers = new ArrayList<>(joinRequest.getPlayers().size()); + for (PlayerView player : joinRequest.getPlayers()) { + ZombiesPlayer zombiesPlayer = zombiesPlayers.get(player.getUUID()); + if (zombiesPlayer != null) { + if (zombiesPlayer.hasQuit()) { + oldPlayers.add(zombiesPlayer); + } + } + else { + newPlayers.add(player); } } - else { - newPlayers.add(player); - } - } - Stage stage = getCurrentStage(); - if (stage == null) { - return TransferResult.failure(Component.text("The game is not currently running.", NamedTextColor.RED)); - } - if (stage.hasPermanentPlayers() && !newPlayers.isEmpty()) { - return TransferResult.failure(Component.text("The game is not accepting new players.", NamedTextColor.RED)); - } + Stage stage = getCurrentStage(); + if (stage == null) { + return TransferResult.failure(Component.text("The game is not currently running.", NamedTextColor.RED)); + } + if (stage.hasPermanentPlayers() && !newPlayers.isEmpty()) { + return TransferResult.failure( + Component.text("The game is not accepting new players.", NamedTextColor.RED)); + } + if (!stage.canRejoin() && !oldPlayers.isEmpty()) { + return TransferResult.failure( + Component.text("The game is not accepting rejoining players.", NamedTextColor.RED)); + } - if (zombiesPlayers.size() + newPlayers.size() > mapSettingsInfo.maxPlayers()) { - return TransferResult.failure(Component.text("Too many players!", NamedTextColor.RED)); - } + if (zombiesPlayers.size() + pendingPlayers + newPlayers.size() > mapSettingsInfo.maxPlayers()) { + return TransferResult.failure(Component.text("Too many players!", NamedTextColor.RED)); + } - TransferResult protocolResult = checkWithinProtocolVersionBounds(newPlayers); - if (protocolResult != null) { - return protocolResult; - } + TransferResult protocolResult = checkWithinProtocolVersionBounds(newPlayers); + if (protocolResult != null) { + return protocolResult; + } - return TransferResult.success(() -> { - Vec3I spawn = mapSettingsInfo.origin().add(mapSettingsInfo.spawn()); - Pos pos = new Pos(spawn.x(), spawn.y(), spawn.z(), mapSettingsInfo.yaw(), mapSettingsInfo.pitch()); - List> teleportedPlayers = new ArrayList<>(oldPlayers.size() + newPlayers.size()); - List> futures = new ArrayList<>(oldPlayers.size() + newPlayers.size()); - List runnables = new ArrayList<>(oldPlayers.size() + newPlayers.size()); - for (ZombiesPlayer zombiesPlayer : oldPlayers) { - zombiesPlayer.getPlayer().ifPresent(player -> { - teleportedPlayers.add(Pair.of(player, player.getInstance())); - if (player.getInstance() == instance) { - futures.add(player.teleport(pos)); - } - else { - futures.add(player.setInstance(instance, pos)); - } - runnables.add(() -> { - zombiesPlayer.setState(ZombiesPlayerStateKeys.DEAD, DeadPlayerStateContext.rejoin()); + pendingPlayers += newPlayers.size(); + + return TransferResult.success(() -> { + Vec3I spawn = mapSettingsInfo.origin().add(mapSettingsInfo.spawn()); + Pos pos = new Pos(spawn.x(), spawn.y(), spawn.z(), mapSettingsInfo.yaw(), mapSettingsInfo.pitch()); + List> teleportedPlayers = new ArrayList<>(oldPlayers.size() + newPlayers.size()); + List> futures = new ArrayList<>(oldPlayers.size() + newPlayers.size()); + List runnables = new ArrayList<>(oldPlayers.size() + newPlayers.size()); + for (ZombiesPlayer zombiesPlayer : oldPlayers) { + zombiesPlayer.getPlayer().ifPresent(player -> { + teleportedPlayers.add(Pair.of(player, player.getInstance())); + if (player.getInstance() == instance) { + futures.add(player.teleport(pos)); + } + else { + Instance oldInstance = player.getInstance(); + player.setInstanceAddCallback( + () -> Utils.handleInstanceTransfer(oldInstance, instance, player, + newInstancePlayer -> !super.hasGhost(newInstancePlayer))); + futures.add(player.setInstance(instance, pos)); + } + runnables.add(() -> { + zombiesPlayer.setState(ZombiesPlayerStateKeys.DEAD, DeadPlayerStateContext.rejoin()); + }); }); - }); - } - for (PlayerView view : newPlayers) { - view.getPlayer().ifPresent(player -> { - teleportedPlayers.add(Pair.of(player, player.getInstance())); - if (player.getInstance() == instance) { - futures.add(player.teleport(pos)); - } - else { - futures.add(player.setInstance(instance, pos)); - } - runnables.add(() -> { - ZombiesPlayer zombiesPlayer = playerCreator.apply(view); - zombiesPlayer.start(); - zombiesPlayer.setState(ZombiesPlayerStateKeys.ALIVE, NoContext.INSTANCE); - zombiesPlayers.put(view.getUUID(), zombiesPlayer); + } + for (PlayerView view : newPlayers) { + view.getPlayer().ifPresent(player -> { + teleportedPlayers.add(Pair.of(player, player.getInstance())); + if (player.getInstance() == instance) { + futures.add(player.teleport(pos)); + } + else { + Instance oldInstance = player.getInstance(); + player.setInstanceAddCallback( + () -> Utils.handleInstanceTransfer(oldInstance, instance, player, + newInstancePlayer -> !super.hasGhost(newInstancePlayer))); + futures.add(player.setInstance(instance, pos)); + } + runnables.add(() -> { + ZombiesPlayer zombiesPlayer = playerCreator.apply(view); + zombiesPlayer.start(); + zombiesPlayer.setState(ZombiesPlayerStateKeys.ALIVE, AlivePlayerStateContext.regular()); + zombiesPlayers.put(view.getUUID(), zombiesPlayer); + }); }); - }); - } + } - CompletableFuture.allOf(futures.toArray(EMPTY_COMPLETABLE_FUTURE_ARRAY)).whenComplete((ignored, error) -> { - for (int i = 0; i < futures.size(); i++) { - Pair pair = teleportedPlayers.get(i); - Player teleportedPlayer = pair.first(); - Instance oldInstance = pair.second(); + CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).whenComplete((ignored, error) -> { + for (int i = 0; i < futures.size(); i++) { + Pair pair = teleportedPlayers.get(i); + Player teleportedPlayer = pair.first(); - CompletableFuture future = futures.get(i); - Runnable runnable = runnables.get(i); + CompletableFuture future = futures.get(i); + Runnable runnable = runnables.get(i); - if (future.isCompletedExceptionally()) { - future.whenComplete((ignored1, throwable) -> { - LOGGER.warn("Failed to send {} to an instance", teleportedPlayer.getUuid(), throwable); - }); - continue; - } - - Utils.handleInstanceTransfer(oldInstance, teleportedPlayer); + if (future.isCompletedExceptionally()) { + future.whenComplete((ignored1, throwable) -> { + LOGGER.warn("Failed to send {} to an instance", teleportedPlayer.getUuid(), throwable); + }); + continue; + } - runnable.run(); - stage.onJoin(zombiesPlayers.get(teleportedPlayer.getUuid())); + runnable.run(); + stage.onJoin(zombiesPlayers.get(teleportedPlayer.getUuid())); + } + }).whenComplete((ignored, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to finish player join", throwable); + } + }).join(); + }, () -> { + synchronized (joinLock) { + pendingPlayers -= newPlayers.size(); } - }).join(); - }); + }); + } } @Override @@ -291,18 +323,35 @@ public int getJoinWeight(@NotNull ZombiesJoinRequest request) { @Override public void shutdown() { + stageTransition.end(); taskScheduler.end(); + + List> fallbackFutures = new ArrayList<>(zombiesPlayers.size()); for (ZombiesPlayer zombiesPlayer : zombiesPlayers.values()) { database.synchronizeZombiesPlayerMapStats(zombiesPlayer.module().getStats()); if (!zombiesPlayer.hasQuit()) { - fallback.fallback(zombiesPlayer.module().getPlayerView()); + fallbackFutures.add(fallback.fallback(zombiesPlayer.module().getPlayerView()) + .whenComplete((fallbackResult, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to fallback {}", zombiesPlayer.getUUID(), throwable); + } + })); } zombiesPlayer.end(); } - super.shutdown(); + super.shutdown = true; + + //wait for all players to fallback before we actually shut down the scene + CompletableFuture.allOf(fallbackFutures.toArray(CompletableFuture[]::new)).whenComplete((ignored, error) -> { + if (error != null) { + LOGGER.warn("Error when waiting on player fallback", error); + } + + super.shutdown(); + }); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneProvider.java b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneProvider.java index c8f622a38..1049e9aa5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneProvider.java +++ b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneProvider.java @@ -7,9 +7,10 @@ import com.github.steanky.element.core.path.ElementPath; import com.github.steanky.proxima.solid.Solid; import com.github.steanky.toolkit.collection.Wrapper; +import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongList; import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; +import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; @@ -19,7 +20,10 @@ import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.player.*; import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.server.play.TeamsPacket; +import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Team; +import net.minestom.server.scoreboard.TeamManager; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.BasicTickTaskScheduler; import org.phantazm.commons.TickTaskScheduler; @@ -30,17 +34,17 @@ import org.phantazm.core.instance.InstanceLoader; import org.phantazm.core.player.PlayerView; import org.phantazm.core.sound.BasicSongPlayer; +import org.phantazm.core.sound.SongLoader; import org.phantazm.core.sound.SongPlayer; import org.phantazm.core.time.AnalogTickFormatter; -import org.phantazm.core.time.DurationTickFormatter; import org.phantazm.core.time.PrecisionSecondTickFormatter; -import org.phantazm.core.time.TickFormatter; import org.phantazm.core.tracker.BoundedTracker; import org.phantazm.mob.MobModel; import org.phantazm.mob.MobStore; import org.phantazm.mob.trigger.EventTrigger; import org.phantazm.mob.trigger.EventTriggers; import org.phantazm.proxima.bindings.minestom.InstanceSpawner; +import org.phantazm.stats.zombies.ZombiesDatabase; import org.phantazm.zombies.Attributes; import org.phantazm.zombies.corpse.CorpseCreator; import org.phantazm.zombies.event.EntityDamageByGunEvent; @@ -59,9 +63,11 @@ import org.phantazm.zombies.sidebar.SidebarModule; import org.phantazm.zombies.sidebar.SidebarUpdater; import org.phantazm.zombies.stage.*; -import org.phantazm.stats.zombies.ZombiesDatabase; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.function.Function; import java.util.function.Supplier; @@ -73,8 +79,6 @@ public class ZombiesSceneProvider extends SceneProviderAbstract rootNode; private final ContextManager contextManager; private final KeyParser keyParser; - private final Team mobNoPushTeam; - private final Team corpseTeam; private final ZombiesDatabase database; private final MapObjects.Source mapObjectSource; @@ -83,18 +87,18 @@ public class ZombiesSceneProvider extends SceneProviderAbstract instanceSpaceFunction, @NotNull MapInfo mapInfo, @NotNull InstanceLoader instanceLoader, @NotNull SceneFallback sceneFallback, @NotNull EventNode rootNode, @NotNull MobSpawnerSource mobSpawnerSource, @NotNull Map mobModels, @NotNull ClientBlockHandlerSource clientBlockHandlerSource, - @NotNull ContextManager contextManager, @NotNull KeyParser keyParser, @NotNull Team mobNoPushTeam, - @NotNull Team corpseTeam, @NotNull ZombiesDatabase database, @NotNull Map powerups, - @NotNull ZombiesPlayer.Source zombiesPlayerSource, @NotNull CorpseCreator.Source corpseCreatorSource) { - super(maximumScenes); + @NotNull ContextManager contextManager, @NotNull KeyParser keyParser, @NotNull ZombiesDatabase database, + @NotNull Map powerups, @NotNull ZombiesPlayer.Source zombiesPlayerSource, + @NotNull CorpseCreator.Source corpseCreatorSource, @NotNull SongLoader songLoader) { + super(executor, maximumScenes); this.instanceSpaceFunction = Objects.requireNonNull(instanceSpaceFunction, "instanceSpaceFunction"); this.mapInfo = Objects.requireNonNull(mapInfo, "mapInfo"); this.instanceLoader = Objects.requireNonNull(instanceLoader, "instanceLoader"); @@ -106,8 +110,6 @@ public ZombiesSceneProvider(int maximumScenes, Objects.requireNonNull(powerups, "powerups"); MapSettingsInfo settingsInfo = mapInfo.settings(); - this.mobNoPushTeam = settingsInfo.mobPlayerCollisions() ? null : mobNoPushTeam; - this.corpseTeam = Objects.requireNonNull(corpseTeam, "corpseTeam"); this.mapObjectSource = new BasicMapObjectsSource(mapInfo, contextManager, mobSpawnerSource, mobModels, clientBlockHandlerSource, keyParser); @@ -117,14 +119,21 @@ public ZombiesSceneProvider(int maximumScenes, this.shopHandlerSource = new BasicShopHandlerSource(); this.windowHandlerSource = new BasicWindowHandlerSource(settingsInfo.windowRepairRadius(), settingsInfo.windowRepairTicks(), - settingsInfo.repairCoins()); + settingsInfo.repairCoins(), new WindowHandler.WindowMessages(settingsInfo.nearWindowMessage(), + settingsInfo.startRepairingMessage(), settingsInfo.stopRepairingMessage(), + settingsInfo.finishRepairingMessage(), settingsInfo.enemiesNearbyMessage())); this.doorHandlerSource = new BasicDoorHandlerSource(); this.corpseCreatorSource = Objects.requireNonNull(corpseCreatorSource, "corpseCreatorSource"); + this.songLoader = Objects.requireNonNull(songLoader, "songLoader"); } @Override protected @NotNull Optional chooseScene(@NotNull ZombiesJoinRequest request) { + if (request.isRestricted()) { + return Optional.empty(); + } + sceneLoop: for (ZombiesScene scene : getScenes()) { if (scene.isComplete() || !scene.isJoinable() || scene.isShutdown()) { @@ -156,7 +165,7 @@ public ZombiesSceneProvider(int maximumScenes, } @Override - protected @NotNull ZombiesScene createScene(@NotNull ZombiesJoinRequest request) { + protected @NotNull CompletableFuture createScene(@NotNull ZombiesJoinRequest request) { Wrapper roundHandlerWrapper = Wrapper.ofNull(); Wrapper powerupHandlerWrapper = Wrapper.ofNull(); Wrapper windowHandlerWrapper = Wrapper.ofNull(); @@ -167,91 +176,115 @@ public ZombiesSceneProvider(int maximumScenes, MapSettingsInfo settings = mapInfo.settings(); Pos spawnPos = VecUtils.toPos(settings.origin().add(settings.spawn())); - Instance instance = instanceLoader.loadInstance(settings.instancePath()); - - instance.setTime(settings.worldTime()); - instance.setTimeRate(0); - - //LinkedHashMap for better value iteration performance - Map zombiesPlayers = new LinkedHashMap<>(settings.maxPlayers()); - - MobStore mobStore = new MobStore(); - TickTaskScheduler tickTaskScheduler = new BasicTickTaskScheduler(); - - SongPlayer songPlayer = new BasicSongPlayer(); - MapObjects mapObjects = - createMapObjects(instance, zombiesPlayers, roundHandlerWrapper, mobStore, mobNoPushTeam, corpseTeam, - powerupHandlerWrapper, windowHandlerWrapper, eventNodeWrapper, songPlayer, tickTaskScheduler); - - RoundHandler roundHandler = new BasicRoundHandler(zombiesPlayers.values(), mapObjects.rounds()); - roundHandlerWrapper.set(roundHandler); - - PowerupHandler powerupHandler = - createPowerupHandler(instance, zombiesPlayers, mapObjects.mapDependencyProvider()); - powerupHandlerWrapper.set(powerupHandler); - - ShopHandler shopHandler = createShopHandler(mapObjects.shopTracker()); - - WindowHandler windowHandler = createWindowHandler(mapObjects.windowTracker(), zombiesPlayers.values()); - windowHandlerWrapper.set(windowHandler); - - DoorHandler doorHandler = createDoorHandler(mapObjects.doorTracker(), mapObjects.roomTracker()); - - ZombiesMap map = - new ZombiesMap(mapObjects, songPlayer, powerupHandler, roundHandler, shopHandler, windowHandler, - doorHandler, mobStore); - - - SidebarModule sidebarModule = - new SidebarModule(zombiesPlayers, zombiesPlayers.values(), roundHandler, ticksSinceStart, - settings.maxPlayers()); - StageTransition stageTransition = - createStageTransition(instance, mapObjects.module().random(), zombiesPlayers.values(), spawnPos, - roundHandler, ticksSinceStart, sidebarModule, shopHandler); - - LeaveHandler leaveHandler = new LeaveHandler(stageTransition, zombiesPlayers); - - - EventNode childNode = - createEventNode(instance, zombiesPlayers, mapObjects, roundHandler, shopHandler, windowHandler, - doorHandler, mapObjects.roomTracker(), mapObjects.windowTracker(), powerupHandler, mobStore, - leaveHandler); - eventNodeWrapper.set(childNode); - - CorpseCreator corpseCreator = createCorpseCreator(mapObjects.mapDependencyProvider()); - + return instanceLoader.loadInstance(settings.instancePath()).thenApply(instance -> { + instance.setTime(settings.worldTime()); + instance.setTimeRate(0); + + Map zombiesPlayers = new ConcurrentHashMap<>(settings.maxPlayers()); + + MobStore mobStore = new MobStore(); + TickTaskScheduler tickTaskScheduler = new BasicTickTaskScheduler(); + + SongPlayer songPlayer = new BasicSongPlayer(); + + TeamManager teamManager = MinecraftServer.getTeamManager(); + Team mobNoPushTeam = + !settings.mobPlayerCollisions() ? teamManager.createBuilder(UUID.randomUUID().toString()) + .collisionRule(TeamsPacket.CollisionRule.PUSH_OTHER_TEAMS).build() : null; + Team corpseTeam = teamManager.createBuilder(UUID.randomUUID().toString()) + .collisionRule(TeamsPacket.CollisionRule.NEVER) + .nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER).build(); + + MapObjects mapObjects = + createMapObjects(instance, zombiesPlayers, roundHandlerWrapper, mobStore, mobNoPushTeam, corpseTeam, + powerupHandlerWrapper, windowHandlerWrapper, eventNodeWrapper, songPlayer, songLoader, + tickTaskScheduler, ticksSinceStart); + + RoundHandler roundHandler = new BasicRoundHandler(zombiesPlayers.values(), mapObjects.rounds()); + roundHandlerWrapper.set(roundHandler); + + PowerupHandler powerupHandler = + createPowerupHandler(instance, zombiesPlayers, mapObjects.mapDependencyProvider()); + powerupHandlerWrapper.set(powerupHandler); + + ShopHandler shopHandler = createShopHandler(mapObjects.shopTracker(), mapObjects.roomTracker()); + + WindowHandler windowHandler = createWindowHandler(mapObjects.windowTracker(), zombiesPlayers.values()); + windowHandlerWrapper.set(windowHandler); + + DoorHandler doorHandler = createDoorHandler(mapObjects.doorTracker(), mapObjects.roomTracker()); + + ZombiesMap map = + new ZombiesMap(mapObjects, songPlayer, powerupHandler, roundHandler, shopHandler, windowHandler, + doorHandler, mobStore); + + SidebarModule sidebarModule = + new SidebarModule(zombiesPlayers, zombiesPlayers.values(), roundHandler, ticksSinceStart, + settings.maxPlayers()); + StageTransition stageTransition = + createStageTransition(instance, mapObjects.module().random(), zombiesPlayers.values(), spawnPos, + roundHandler, ticksSinceStart, sidebarModule, shopHandler); + stageTransition.start(); + + LeaveHandler leaveHandler = new LeaveHandler(stageTransition, zombiesPlayers); + + + EventNode childNode = + createEventNode(instance, zombiesPlayers, mapObjects, roundHandler, shopHandler, windowHandler, + doorHandler, mapObjects.roomTracker(), mapObjects.windowTracker(), powerupHandler, mobStore, + leaveHandler); + eventNodeWrapper.set(childNode); + + CorpseCreator corpseCreator = createCorpseCreator(mapObjects.mapDependencyProvider()); + BelowNameTag belowNameTag = new BelowNameTag(UUID.randomUUID().toString(), settings.healthDisplay()); + Function playerCreator = playerView -> { + playerView.getPlayer().ifPresent(player -> { + player.getAttribute(Attributes.HEAL_TICKS).setBaseValue(settings.healTicks()); + player.getAttribute(Attributes.REVIVE_TICKS).setBaseValue(settings.baseReviveTicks()); + }); + + return zombiesPlayerSource.createPlayer(sceneWrapper.get(), zombiesPlayers, settings, + mapInfo.playerCoins(), mapInfo.leaderboard(), instance, playerView, + mapObjects.module().modifierSource(), new BasicFlaggable(), childNode, + mapObjects.module().random(), mapObjects, mobStore, mapObjects.mobSpawner(), corpseCreator, + belowNameTag); + }; + + UUID allowedRequestUUID = request.isRestricted() ? request.getUUID() : null; + ZombiesScene scene = + new ZombiesScene(UUID.randomUUID(), map, zombiesPlayers, instance, sceneFallback, settings, + stageTransition, leaveHandler, playerCreator, tickTaskScheduler, database, childNode, + allowedRequestUUID); + sceneWrapper.set(scene); + rootNode.addChild(childNode); + + InstanceSpawner.InstanceSettings instanceSettings = instanceSpaceFunction.apply(instance); + instanceSettings.spaceHandler().space().setOverrideFunction((x, y, z) -> { + if (windowHandler.tracker().atPoint(x, y, z).isPresent()) { + return Solid.EMPTY; + } - Function playerCreator = playerView -> { - playerView.getPlayer().ifPresent(player -> { - player.getAttribute(Attributes.HEAL_TICKS).setBaseValue(settings.healTicks()); - player.getAttribute(Attributes.REVIVE_TICKS).setBaseValue(settings.baseReviveTicks()); + return null; }); - return zombiesPlayerSource.createPlayer(sceneWrapper.get(), zombiesPlayers, settings, instance, playerView, - mapObjects.module().modifierSource(), new BasicFlaggable(), childNode, mapObjects.module().random(), - mapObjects, mobStore, mapObjects.mobSpawner(), corpseCreator); - }; - - ZombiesScene scene = new ZombiesScene(UUID.randomUUID(), map, zombiesPlayers, instance, sceneFallback, settings, - stageTransition, leaveHandler, playerCreator, tickTaskScheduler, database, childNode); - sceneWrapper.set(scene); - rootNode.addChild(childNode); - - InstanceSpawner.InstanceSettings instanceSettings = instanceSpaceFunction.apply(instance); - instanceSettings.spaceHandler().space().setOverrideFunction((x, y, z) -> { - if (windowHandler.tracker().atPoint(x, y, z).isPresent()) { - return Solid.EMPTY; - } - - return null; + return scene; }); - - return scene; } @Override protected void cleanupScene(@NotNull ZombiesScene scene) { rootNode.removeChild(scene.getSceneNode()); + MapObjects mapObjects = scene.getMap().mapObjects(); + + Team mobNoPush = mapObjects.mobNoPushTeam(); + Team corpseTeam = mapObjects.corpseTeam(); + + TeamManager manager = MinecraftServer.getTeamManager(); + if (mobNoPush != null) { + manager.deleteTeam(mobNoPush); + } + + manager.deleteTeam(corpseTeam); } private CorpseCreator createCorpseCreator(DependencyProvider mapDependencyProvider) { @@ -261,9 +294,11 @@ private CorpseCreator createCorpseCreator(DependencyProvider mapDependencyProvid private MapObjects createMapObjects(Instance instance, Map zombiesPlayers, Supplier roundHandlerSupplier, MobStore mobStore, Team mobNoPushTeam, Team corpseTeam, Wrapper powerupHandler, Wrapper windowHandler, - Wrapper> eventNode, SongPlayer songPlayer, TickTaskScheduler tickTaskScheduler) { + Wrapper> eventNode, SongPlayer songPlayer, SongLoader songLoader, + TickTaskScheduler tickTaskScheduler, Wrapper ticksSinceStart) { return mapObjectSource.make(instance, zombiesPlayers, roundHandlerSupplier, mobStore, mobNoPushTeam, - powerupHandler, windowHandler, eventNode, songPlayer, tickTaskScheduler, corpseTeam); + powerupHandler, windowHandler, eventNode, songPlayer, songLoader, tickTaskScheduler, corpseTeam, + ticksSinceStart); } private PowerupHandler createPowerupHandler(Instance instance, Map playerMap, @@ -271,8 +306,8 @@ private PowerupHandler createPowerupHandler(Instance instance, Map shopTracker) { - return shopHandlerSource.make(shopTracker); + private ShopHandler createShopHandler(BoundedTracker shopTracker, BoundedTracker rooms) { + return shopHandlerSource.make(shopTracker, rooms); } private WindowHandler createWindowHandler(BoundedTracker windowTracker, @@ -290,7 +325,7 @@ private DoorHandler createDoorHandler(BoundedTracker doorTracker, BoundedT @NotNull DoorHandler doorHandler, @NotNull BoundedTracker roomTracker, @NotNull BoundedTracker windowTracker, @NotNull PowerupHandler powerupHandler, @NotNull MobStore mobStore, @NotNull LeaveHandler leaveHandler) { - EventNode node = EventNode.all("phantazm_zombies_instance_{" + instance.getUniqueId() + "}"); + EventNode node = EventNode.all("phantazm_zombies_instance_" + instance.getUniqueId()); MapSettingsInfo settings = mapInfo.settings(); //entity events @@ -305,7 +340,7 @@ private DoorHandler createDoorHandler(BoundedTracker doorTracker, BoundedT node.addListener(EntityDamageEvent.class, new PlayerDamageEventListener(instance, zombiesPlayers, mapObjects)); node.addListener(PlayerHandAnimationEvent.class, new PlayerLeftClickListener(instance, zombiesPlayers, mobStore, settings.punchDamage(), - settings.punchRange())); + settings.punchRange(), settings.punchCooldown(), settings.punchKnockback())); node.addListener(PlayerChangeHeldSlotEvent.class, new PlayerItemSelectListener(instance, zombiesPlayers)); node.addListener(ItemDropEvent.class, new PlayerDropItemListener(instance, zombiesPlayers)); node.addListener(PlayerDisconnectEvent.class, new PlayerQuitListener(instance, zombiesPlayers, leaveHandler)); @@ -344,21 +379,26 @@ private DoorHandler createDoorHandler(BoundedTracker doorTracker, BoundedT @NotNull Collection zombiesPlayers, @NotNull Pos spawnPos, @NotNull RoundHandler roundHandler, @NotNull Wrapper ticksSinceStart, @NotNull SidebarModule sidebarModule, @NotNull ShopHandler shopHandler) { - Stage idle = new IdleStage(zombiesPlayers, newSidebarUpdaterCreator(sidebarModule, ElementPath.of("idle"))); + MapSettingsInfo settings = mapInfo.settings(); - LongList alertTicks = LongList.of(400L, 200L, 100L, 80L, 60L, 40L, 20L); + Stage idle = new IdleStage(instance, settings, zombiesPlayers, + newSidebarUpdaterCreator(sidebarModule, ElementPath.of("idle")), settings.idleRevertTicks()); - MapSettingsInfo settings = mapInfo.settings(); - Stage countdown = - new CountdownStage(instance, zombiesPlayers, settings, random, 400L, alertTicks, - new PrecisionSecondTickFormatter(new PrecisionSecondTickFormatter.Data(0)), - newSidebarUpdaterCreator(sidebarModule, ElementPath.of("countdown"))); - Stage inGame = new InGameStage(instance, zombiesPlayers, settings, spawnPos, roundHandler, ticksSinceStart, - settings.defaultEquipment(), settings.equipmentGroups().keySet(), - newSidebarUpdaterCreator(sidebarModule, ElementPath.of("inGame")), shopHandler, - new AnalogTickFormatter(new AnalogTickFormatter.Data(false))); - Stage end = new EndStage(instance, zombiesPlayers, 200L, - newSidebarUpdaterCreator(sidebarModule, ElementPath.of("end"))); + LongList countdownAlertTicks = new LongArrayList(settings.countdownAlertTicks()); + + Stage countdown = new CountdownStage(instance, zombiesPlayers, settings, random, settings.countdownTicks(), + countdownAlertTicks, new PrecisionSecondTickFormatter(new PrecisionSecondTickFormatter.Data(0)), + newSidebarUpdaterCreator(sidebarModule, ElementPath.of("countdown"))); + + Stage inGame = + new InGameStage(zombiesPlayers, spawnPos, roundHandler, ticksSinceStart, settings.defaultEquipment(), + settings.equipmentGroups().keySet(), + newSidebarUpdaterCreator(sidebarModule, ElementPath.of("inGame")), shopHandler); + + Stage end = new EndStage(instance, settings, mapInfo.webhook(), + new AnalogTickFormatter(new AnalogTickFormatter.Data(false)), zombiesPlayers, + Wrapper.of(settings.endTicks()), ticksSinceStart, + newSidebarUpdaterCreator(sidebarModule, ElementPath.of("end")), roundHandler); return new StageTransition(idle, countdown, inGame, end); } diff --git a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneRouter.java b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneRouter.java index d69e617dc..249f2328b 100644 --- a/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneRouter.java +++ b/zombies/src/main/java/org/phantazm/zombies/scene/ZombiesSceneRouter.java @@ -2,15 +2,20 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Namespaces; import org.phantazm.core.game.scene.RouteResult; import org.phantazm.core.game.scene.SceneProvider; import org.phantazm.core.game.scene.SceneRouter; import org.phantazm.zombies.player.ZombiesPlayer; import java.util.*; +import java.util.concurrent.CompletableFuture; public class ZombiesSceneRouter implements SceneRouter { + public static final Key KEY = Key.key(Namespaces.PHANTAZM, "zombies"); + private final Map> sceneProviders; private boolean shutdown = false; @@ -58,29 +63,34 @@ public void tick(long time) { } @Override - public @NotNull RouteResult findScene(@NotNull ZombiesRouteRequest routeRequest) { + public @NotNull CompletableFuture> findScene(@NotNull ZombiesRouteRequest routeRequest) { if (isShutdown()) { - return RouteResult.failure(Component.text("The router is shutdown.")); + return CompletableFuture.completedFuture( + RouteResult.failure(Component.text("This game has shut down.", NamedTextColor.RED))); } if (!isJoinable()) { - return RouteResult.failure(Component.text("The router is not joinable.")); + return CompletableFuture.completedFuture( + RouteResult.failure(Component.text("This game is not " + "joinable.", NamedTextColor.RED))); } if (routeRequest.targetMap() != null) { return joinGame(routeRequest); } - return rejoinGame(routeRequest); + return CompletableFuture.completedFuture(rejoinGame(routeRequest)); } - private RouteResult joinGame(ZombiesRouteRequest routeRequest) { + private CompletableFuture> joinGame(ZombiesRouteRequest routeRequest) { SceneProvider sceneProvider = sceneProviders.get(routeRequest.targetMap()); if (sceneProvider == null) { - return RouteResult.failure(Component.text("No games exist with key " + routeRequest.targetMap() + ".")); + return CompletableFuture.completedFuture(RouteResult.failure( + Component.text("No games exist with key " + routeRequest.targetMap() + ".", NamedTextColor.RED))); } - return sceneProvider.provideScene(routeRequest.joinRequest()).map(RouteResult::success) - .orElseGet(() -> RouteResult.failure(Component.text("No games are joinable."))); + return sceneProvider.provideScene(routeRequest.joinRequest()).thenApply(sceneOptional -> { + return sceneOptional.map(RouteResult::success) + .orElseGet(() -> RouteResult.failure(Component.text("No games are joinable.", NamedTextColor.RED))); + }); } private RouteResult rejoinGame(ZombiesRouteRequest routeRequest) { @@ -138,4 +148,9 @@ public boolean hasActiveScenes() { return false; } + + @Override + public @NotNull Key key() { + return KEY; + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/SidebarUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/SidebarUpdater.java index 15afef726..8a348c8f0 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/SidebarUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/SidebarUpdater.java @@ -13,6 +13,7 @@ import java.util.Optional; @Model("zombies.sidebar.updater") +@Cache(false) public class SidebarUpdater implements Activable { public static final int MAX_SIDEBAR_ROWS = 15; diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConditionalSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConditionalSidebarLineUpdater.java index 55e669da3..056f36229 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConditionalSidebarLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConditionalSidebarLineUpdater.java @@ -10,6 +10,7 @@ import java.util.function.BooleanSupplier; @Model("zombies.sidebar.line_updater.conditional") +@Cache(false) public class ConditionalSidebarLineUpdater implements SidebarLineUpdater { @@ -39,6 +40,7 @@ public void invalidateCache() { } @Model("zombies.sidebar.line_updater.conditional.child") + @Cache(false) public static class ChildUpdater { private final BooleanSupplier condition; diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConstantSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConstantSidebarLineUpdater.java index d3075733f..eef857ef3 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConstantSidebarLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/ConstantSidebarLineUpdater.java @@ -1,5 +1,6 @@ package org.phantazm.zombies.sidebar.lineupdater; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; @@ -10,6 +11,7 @@ import java.util.Optional; @Model("zombies.sidebar.line_updater.constant") +@Cache(false) public class ConstantSidebarLineUpdater implements SidebarLineUpdater { private final Data data; diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/DateLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/DateLineUpdater.java index 8e94abe63..f754d9f11 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/DateLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/DateLineUpdater.java @@ -5,11 +5,12 @@ import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.Optional; @@ -17,13 +18,11 @@ @Cache(false) public class DateLineUpdater implements SidebarLineUpdater { private final Data data; - private final DateTimeFormatter formatter; private Component cached; @FactoryMethod public DateLineUpdater(@NotNull Data data) { this.data = Objects.requireNonNull(data, "date"); - this.formatter = DateTimeFormatter.ofPattern(data.dateFormat); this.cached = null; } @@ -35,14 +34,15 @@ public void invalidateCache() { @Override public @NotNull Optional tick(long time) { if (cached == null) { - cached = Component.text(formatter.format(LocalDateTime.now())).style(data.dateStyle); + TagResolver datePlaceholder = Formatter.date("date", LocalDateTime.now()); + cached = MiniMessage.miniMessage().deserialize(data.format, datePlaceholder); } return Optional.of(cached); } @DataObject - public record Data(@NotNull Style dateStyle, @NotNull String dateFormat) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/JoinedPlayersSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/JoinedPlayersSidebarLineUpdater.java index 720fd1ec3..caf4b2471 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/JoinedPlayersSidebarLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/JoinedPlayersSidebarLineUpdater.java @@ -6,6 +6,8 @@ import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.player.ZombiesPlayer; @@ -40,22 +42,20 @@ public void invalidateCache() { if (currentPlayers == -1 || currentPlayers != zombiesPlayers.size()) { currentPlayers = zombiesPlayers.size(); - String formatted; - try { - formatted = String.format(data.formatString, currentPlayers, maxPlayers); - } - catch (IllegalFormatException ignored) { - formatted = data.formatString; - } + TagResolver currentPlayerCountPlaceholder = + Placeholder.component("current_player_count", Component.text(currentPlayers)); + TagResolver maxPlayerCountPlaceholder = + Placeholder.component("max_player_count", Component.text(maxPlayers)); - return Optional.of(MiniMessage.miniMessage().deserialize(formatted)); + return Optional.of(MiniMessage.miniMessage() + .deserialize(data.format, currentPlayerCountPlaceholder, maxPlayerCountPlaceholder)); } return Optional.empty(); } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/PlayerStateSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/PlayerStateSidebarLineUpdater.java deleted file mode 100644 index e236d8e40..000000000 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/PlayerStateSidebarLineUpdater.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.phantazm.zombies.sidebar.lineupdater; - -import com.github.steanky.element.core.annotation.Cache; -import com.github.steanky.element.core.annotation.FactoryMethod; -import com.github.steanky.element.core.annotation.Model; -import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; -import org.phantazm.core.player.PlayerView; -import org.phantazm.zombies.player.state.PlayerStateSwitcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -@Model("zombies.sidebar.line_updater.player_state") -@Cache(false) -public class PlayerStateSidebarLineUpdater implements SidebarLineUpdater { - private static final Logger LOGGER = LoggerFactory.getLogger(PlayerStateSidebarLineUpdater.class); - private static final int DISPLAY_REFRESH_INTERVAL = 2000; - - private final PlayerView playerView; - - private final PlayerStateSwitcher stateSwitcher; - - private CompletableFuture playerNameFuture; - - private long lastDisplayRefresh; - - @FactoryMethod - public PlayerStateSidebarLineUpdater(@NotNull PlayerView playerView, @NotNull PlayerStateSwitcher stateSwitcher) { - this.playerView = Objects.requireNonNull(playerView, "playerView"); - this.stateSwitcher = Objects.requireNonNull(stateSwitcher, "stateSwitcher"); - } - - @Override - public void invalidateCache() { - - } - - @Override - public @NotNull Optional tick(long time) { - if (time - lastDisplayRefresh >= DISPLAY_REFRESH_INTERVAL) { - playerNameFuture = null; - lastDisplayRefresh = time; - } - - if (playerNameFuture == null) { - playerNameFuture = playerView.getDisplayName(); - } - - try { - Component playerName = playerNameFuture.getNow(null); - if (playerName == null) { - return Optional.empty(); - } - - return Optional.of(Component.textOfChildren(playerName, Component.text(": "), - stateSwitcher.getState().getDisplayName())); - } - catch (Throwable e) { - LOGGER.warn("Error resolving player name", e); - playerNameFuture = null; - } - - return Optional.empty(); - } -} diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RemainingZombiesSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RemainingZombiesSidebarLineUpdater.java index e14536a69..8d47b15f5 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RemainingZombiesSidebarLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RemainingZombiesSidebarLineUpdater.java @@ -1,10 +1,13 @@ package org.phantazm.zombies.sidebar.lineupdater; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.Round; import org.phantazm.zombies.map.handler.RoundHandler; @@ -14,6 +17,7 @@ import java.util.function.Supplier; @Model("zombies.sidebar.line_updater.remaining_zombies") +@Cache(false) public class RemainingZombiesSidebarLineUpdater implements SidebarLineUpdater { private final Data data; private final Supplier> roundSupplier; @@ -36,7 +40,9 @@ public void invalidateCache() { int totalMobCount = round.getTotalMobCount(); if ((lastRemainingZombies == -1 || lastRemainingZombies != totalMobCount)) { lastRemainingZombies = totalMobCount; - return MiniMessage.miniMessage().deserialize(String.format(data.formatString, lastRemainingZombies)); + + TagResolver remainingZombiesPlaceholder = Placeholder.component("remaining_zombies", Component.text(lastRemainingZombies)); + return MiniMessage.miniMessage().deserialize(data.format, remainingZombiesPlaceholder); } return null; @@ -44,6 +50,6 @@ public void invalidateCache() { } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RoundSidebarLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RoundSidebarLineUpdater.java index 2975b4bf5..c58f86954 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RoundSidebarLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/RoundSidebarLineUpdater.java @@ -1,10 +1,13 @@ package org.phantazm.zombies.sidebar.lineupdater; +import com.github.steanky.element.core.annotation.Cache; import com.github.steanky.element.core.annotation.DataObject; import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.map.handler.RoundHandler; @@ -12,6 +15,7 @@ import java.util.Optional; @Model("zombies.sidebar.line_updater.round") +@Cache(false) public class RoundSidebarLineUpdater implements SidebarLineUpdater { private final Data data; @@ -36,14 +40,15 @@ public void invalidateCache() { int newIndex = roundHandler.currentRoundIndex(); if ((lastRoundIndex == -1 || lastRoundIndex != newIndex) && newIndex != -1) { lastRoundIndex = newIndex; - return Optional.of(MiniMessage.miniMessage().deserialize( - String.format(data.formatString, Math.min(lastRoundIndex + 1, roundHandler.roundCount())))); + TagResolver roundPlaceholder = Placeholder.component("round", Component.text(Math.min(lastRoundIndex + 1, + roundHandler.roundCount()))); + return Optional.of(MiniMessage.miniMessage().deserialize(data.format, roundPlaceholder)); } return Optional.empty(); } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/TicksLineUpdater.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/TicksLineUpdater.java index 942f7e747..ed4faaaef 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/TicksLineUpdater.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/TicksLineUpdater.java @@ -3,8 +3,10 @@ import com.github.steanky.element.core.annotation.*; import com.github.steanky.toolkit.collection.Wrapper; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; -import org.phantazm.core.ComponentUtils; import org.phantazm.core.time.TickFormatter; import java.util.Objects; @@ -16,6 +18,7 @@ public class TicksLineUpdater implements SidebarLineUpdater { private final Data data; private final Wrapper ticksWrapper; private final TickFormatter tickFormatter; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); private long lastTicks = -1; @FactoryMethod @@ -36,15 +39,15 @@ public void invalidateCache() { if (lastTicks == -1 || lastTicks != ticksWrapper.get()) { lastTicks = ticksWrapper.get(); - - return Optional.of(ComponentUtils.tryFormat(data.formatString, tickFormatter.format(ticksWrapper.get()))); + TagResolver timePlaceholder = Placeholder.unparsed("time", tickFormatter.format(ticksWrapper.get())); + return Optional.of(miniMessage.deserialize(data.format, timePlaceholder)); } return Optional.empty(); } @DataObject - public record Data(@NotNull String formatString, @NotNull @ChildPath("tick_formatter") String tickFormatterPath) { + public record Data(@NotNull String format, @NotNull @ChildPath("tick_formatter") String tickFormatterPath) { public Data { Objects.requireNonNull(tickFormatterPath, "tickFormatterPath"); diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/CoinsUpdaterCreator.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/CoinsUpdaterCreator.java index febe9964c..76068c9a4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/CoinsUpdaterCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/CoinsUpdaterCreator.java @@ -6,6 +6,8 @@ import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.core.player.PlayerView; import org.phantazm.zombies.coin.PlayerCoins; @@ -38,7 +40,7 @@ private static class Updater implements SidebarLineUpdater { private final PlayerView playerView; private final PlayerCoins coins; - private CompletableFuture playerNameFuture; + private CompletableFuture playerNameFuture; private int lastCoins; private boolean cacheInvalidated; @@ -58,14 +60,14 @@ public void invalidateCache() { @Override public @NotNull Optional tick(long time) { if (playerNameFuture == null) { - playerNameFuture = playerView.getUsername(); + playerNameFuture = playerView.getDisplayName(); } if (!playerNameFuture.isDone()) { return Optional.empty(); } - String playerName = playerNameFuture.getNow(null); + Component playerName = playerNameFuture.getNow(null); if (playerName == null) { return Optional.empty(); } @@ -75,8 +77,11 @@ public void invalidateCache() { lastCoins = newCoins; cacheInvalidated = false; + TagResolver playerPlaceholder = Placeholder.component("player", playerName); + TagResolver coinsPlaceholder = Placeholder.component("coins", Component.text(newCoins)); + return Optional.of( - MiniMessage.miniMessage().deserialize(String.format(data.formatString, playerName, newCoins))); + MiniMessage.miniMessage().deserialize(data.format, playerPlaceholder, coinsPlaceholder)); } return Optional.empty(); @@ -84,6 +89,6 @@ public void invalidateCache() { } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/StateUpdaterCreator.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/StateUpdaterCreator.java index 8df44ce39..96344b4fc 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/StateUpdaterCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/StateUpdaterCreator.java @@ -5,8 +5,9 @@ import com.github.steanky.element.core.annotation.FactoryMethod; import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.core.player.PlayerView; import org.phantazm.zombies.player.ZombiesPlayer; @@ -38,7 +39,7 @@ private static class Updater implements SidebarLineUpdater { private final PlayerView playerView; private final PlayerStateSwitcher stateSwitcher; - private CompletableFuture playerNameFuture; + private CompletableFuture playerNameFuture; private ZombiesPlayerState lastState; private boolean cacheInvalidated; @@ -59,14 +60,14 @@ public void invalidateCache() { @Override public @NotNull Optional tick(long time) { if (playerNameFuture == null) { - playerNameFuture = playerView.getUsername(); + playerNameFuture = playerView.getDisplayName(); } if (!playerNameFuture.isDone()) { return Optional.empty(); } - String playerName = playerNameFuture.getNow(null); + Component playerName = playerNameFuture.getNow(null); if (playerName == null) { return Optional.empty(); } @@ -76,9 +77,10 @@ public void invalidateCache() { this.lastState = currentState; this.cacheInvalidated = false; - Component first = MiniMessage.miniMessage().deserialize(String.format(data.formatString, playerName)); - return Optional.of( - Component.join(JoinConfiguration.noSeparators(), first, currentState.getDisplayName())); + TagResolver playerPlaceholder = Placeholder.component("player", playerName); + TagResolver statePlaceholder = Placeholder.component("state", currentState.getDisplayName()); + Component message = MiniMessage.miniMessage().deserialize(data.format, playerPlaceholder, statePlaceholder); + return Optional.of(message); } return Optional.empty(); @@ -86,6 +88,6 @@ public void invalidateCache() { } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/ZombieKillsUpdaterCreator.java b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/ZombieKillsUpdaterCreator.java index 3784a83bc..a581bf062 100644 --- a/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/ZombieKillsUpdaterCreator.java +++ b/zombies/src/main/java/org/phantazm/zombies/sidebar/lineupdater/creator/ZombieKillsUpdaterCreator.java @@ -6,6 +6,8 @@ import com.github.steanky.element.core.annotation.Model; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.phantazm.zombies.kill.PlayerKills; import org.phantazm.zombies.player.ZombiesPlayer; @@ -49,7 +51,9 @@ public void invalidateCache() { public @NotNull Optional tick(long time) { if (killCount == -1 || killCount != playerKills.getKills()) { killCount = playerKills.getKills(); - return Optional.of(MiniMessage.miniMessage().deserialize(String.format(data.formatString, killCount))); + + TagResolver killsPlaceholder = Placeholder.component("kills", Component.text(killCount)); + return Optional.of(MiniMessage.miniMessage().deserialize(data.format, killsPlaceholder)); } return Optional.empty(); @@ -57,6 +61,6 @@ public void invalidateCache() { } @DataObject - public record Data(@NotNull String formatString) { + public record Data(@NotNull String format) { } } diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/CountdownStage.java b/zombies/src/main/java/org/phantazm/zombies/stage/CountdownStage.java index b1108871f..7b8294bdb 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/CountdownStage.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/CountdownStage.java @@ -3,14 +3,14 @@ import com.github.steanky.toolkit.collection.Wrapper; import it.unimi.dsi.fastutil.longs.LongList; import net.kyori.adventure.key.Key; -import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.title.TitlePart; import net.minestom.server.instance.Instance; -import net.minestom.server.sound.SoundEvent; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.chat.MessageWithDestination; import org.phantazm.core.time.TickFormatter; import org.phantazm.zombies.map.MapSettingsInfo; import org.phantazm.zombies.player.ZombiesPlayer; @@ -52,7 +52,7 @@ protected CountdownStage(@NotNull Instance instance, @NotNull Collection { - if (!settings.introMessages().isEmpty()) { - player.sendMessage(settings.introMessages().get(random.nextInt(settings.introMessages().size()))); + if (settings.introMessages().isEmpty()) { + return; } + + List messages = settings.introMessages().get(random.nextInt(settings.introMessages().size())); + for (MessageWithDestination message : messages) { + switch (message.destination()) { + case TITLE -> player.sendTitlePart(TitlePart.TITLE, message.component()); + case SUBTITLE -> player.sendTitlePart(TitlePart.SUBTITLE, message.component()); + case CHAT -> player.sendMessage(message.component()); + case ACTION_BAR -> player.sendActionBar(message.component()); + } + } + }); + + zombiesPlayer.module().getLeaderboard().startIfNotActive(); + int count = zombiesPlayers.size(), maxPlayers = settings.maxPlayers(); + TagResolver countPlaceholder = Placeholder.component("count", Component.text(count)); + TagResolver maxPlayersPlaceholder = Placeholder.component("max_players", Component.text(maxPlayers)); + zombiesPlayer.module().getPlayerView().getDisplayName().thenAccept(displayName -> { + TagResolver joinerPlaceholder = Placeholder.component("joiner", displayName); + Component message = MiniMessage.miniMessage() + .deserialize(settings.gameJoinFormat(), joinerPlaceholder, countPlaceholder, maxPlayersPlaceholder); + instance.sendMessage(message); }); } @@ -90,6 +116,8 @@ public void onLeave(@NotNull ZombiesPlayer zombiesPlayer) { if (updater != null) { updater.end(); } + + zombiesPlayer.module().getLeaderboard().endIfActive(); } @Override @@ -97,9 +125,35 @@ public boolean hasPermanentPlayers() { return false; } + @Override + public boolean canRejoin() { + return false; + } + @Override public void start() { ticksRemaining.set(initialTicks); + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + zombiesPlayer.getPlayer().ifPresent(player -> { + if (settings.introMessages().isEmpty()) { + return; + } + + List messages = settings.introMessages().get(random.nextInt(settings.introMessages().size())); + for (MessageWithDestination message : messages) { + switch (message.destination()) { + case TITLE -> player.sendTitlePart(TitlePart.TITLE, message.component()); + case SUBTITLE -> player.sendTitlePart(TitlePart.SUBTITLE, message.component()); + case CHAT -> player.sendMessage(message.component()); + case ACTION_BAR -> player.sendActionBar(message.component()); + } + } + }); + } } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/EndStage.java b/zombies/src/main/java/org/phantazm/zombies/stage/EndStage.java index 42dcad938..49bcef54c 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/EndStage.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/EndStage.java @@ -1,43 +1,84 @@ package org.phantazm.zombies.stage; import com.github.steanky.toolkit.collection.Wrapper; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.title.TitlePart; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.sound.SoundEvent; import org.jetbrains.annotations.NotNull; +import org.phantazm.core.time.TickFormatter; +import org.phantazm.stats.zombies.ZombiesPlayerMapStats; +import org.phantazm.zombies.map.MapSettingsInfo; +import org.phantazm.zombies.map.WebhookInfo; +import org.phantazm.zombies.map.handler.RoundHandler; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.state.ZombiesPlayerStateKeys; import org.phantazm.zombies.player.state.context.DeadPlayerStateContext; import org.phantazm.zombies.sidebar.SidebarUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.text.MessageFormat; +import java.time.Instant; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; public class EndStage implements Stage { + private static final Logger LOGGER = LoggerFactory.getLogger(EndStage.class); + private static final CompletableFuture[] EMPTY_COMPLETABLE_FUTURE_ARRAY = new CompletableFuture[0]; + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); private final Instance instance; - + private final MapSettingsInfo settings; + private final WebhookInfo webhook; + private final TickFormatter tickFormatter; private final Collection zombiesPlayers; - private final Map sidebarUpdaters = new HashMap<>(); - private final Wrapper remainingTicks; + private final Wrapper ticksSinceStart; private final Function sidebarUpdaterCreator; + private final RoundHandler roundHandler; + + private final Map sidebarUpdaters; + private final HttpClient client = HttpClient.newHttpClient(); - public EndStage(@NotNull Instance instance, @NotNull Collection zombiesPlayers, - @NotNull Wrapper remainingTicks, - @NotNull Function sidebarUpdaterCreator) { + private boolean hasWon; + + public EndStage(@NotNull Instance instance, @NotNull MapSettingsInfo settings, @NotNull WebhookInfo webhook, + @NotNull TickFormatter tickFormatter, @NotNull Collection zombiesPlayers, + @NotNull Wrapper remainingTicks, @NotNull Wrapper ticksSinceStart, + @NotNull Function sidebarUpdaterCreator, + @NotNull RoundHandler roundHandler) { this.instance = Objects.requireNonNull(instance, "instance"); + this.settings = Objects.requireNonNull(settings, "settings"); + this.webhook = Objects.requireNonNull(webhook, "webhook"); + this.tickFormatter = Objects.requireNonNull(tickFormatter, "tickFormatter"); this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); this.remainingTicks = Objects.requireNonNull(remainingTicks, "remainingTicks"); + this.ticksSinceStart = Objects.requireNonNull(ticksSinceStart, "ticksSinceStart"); this.sidebarUpdaterCreator = Objects.requireNonNull(sidebarUpdaterCreator, "sidebarUpdaterCreator"); + this.roundHandler = Objects.requireNonNull(roundHandler, "roundHandler"); + + this.sidebarUpdaters = new HashMap<>(); } - public EndStage(@NotNull Instance instance, @NotNull Collection zombiesPlayers, - long endTicks, Function sidebarUpdaterCreator) { - this(instance, zombiesPlayers, Wrapper.of(endTicks), sidebarUpdaterCreator); + public long ticksSinceStart() { + return ticksSinceStart.get(); } @Override @@ -50,6 +91,11 @@ public boolean shouldRevert() { return false; } + @Override + public boolean shouldAbort() { + return false; + } + @Override public void onJoin(@NotNull ZombiesPlayer zombiesPlayer) { zombiesPlayer.module().getMeta().setInGame(false); @@ -62,6 +108,11 @@ public void onLeave(@NotNull ZombiesPlayer zombiesPlayer) { @Override public boolean hasPermanentPlayers() { + return true; + } + + @Override + public boolean canRejoin() { return false; } @@ -69,13 +120,126 @@ public boolean hasPermanentPlayers() { public void start() { instance.playSound(Sound.sound(SoundEvent.ENTITY_ENDER_DRAGON_DEATH, Sound.Source.MASTER, 1.0F, 1.0F)); + boolean anyAlive = false; + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (zombiesPlayer.isAlive()) { + anyAlive = true; + break; + } + } + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { zombiesPlayer.module().getMeta().setInGame(false); if (zombiesPlayer.isState(ZombiesPlayerStateKeys.KNOCKED)) { - zombiesPlayer.setState(ZombiesPlayerStateKeys.DEAD, DeadPlayerStateContext.killed(null, null)); + Point deathLocation = zombiesPlayer.getPlayer().map(Player::getPosition).orElse(null); + zombiesPlayer.setState(ZombiesPlayerStateKeys.DEAD, + DeadPlayerStateContext.killed(deathLocation, null, null)); } } + + String timeString = tickFormatter.format(ticksSinceStart.get()); + Component finalTime = Component.text(timeString); + int bestRound = roundHandler.currentRoundIndex(); + + TagResolver roundPlaceholder = Placeholder.component("round", Component.text(bestRound + 1)); + if (anyAlive) { + instance.sendTitlePart(TitlePart.TITLE, + MINI_MESSAGE.deserialize(settings.winTitleFormat(), roundPlaceholder)); + instance.sendTitlePart(TitlePart.SUBTITLE, + MINI_MESSAGE.deserialize(settings.winSubtitleFormat(), roundPlaceholder)); + + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + ZombiesPlayerMapStats stats = zombiesPlayer.module().getStats(); + stats.setWins(stats.getWins() + 1); + stats.getBestTime().ifPresentOrElse(prevBest -> { + if (ticksSinceStart.get() < prevBest) { + stats.setBestTime(ticksSinceStart.get()); + } + }, () -> stats.setBestTime(ticksSinceStart.get())); + } + + this.hasWon = true; + runWebhook(timeString); + } + else { + instance.sendTitlePart(TitlePart.TITLE, + MINI_MESSAGE.deserialize(settings.lossTitleFormat(), roundPlaceholder)); + instance.sendTitlePart(TitlePart.SUBTITLE, + MINI_MESSAGE.deserialize(settings.lossSubtitleFormat(), roundPlaceholder)); + + this.hasWon = false; + } + + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (zombiesPlayer.hasQuit()) { + continue; + } + + ZombiesPlayerMapStats stats = zombiesPlayer.module().getStats(); + int gunAccuracy = stats.getShots() <= 0 + ? 100 + : (int)Math.rint(((double)(stats.getRegularHits() + stats.getHeadshotHits()) / + (double)stats.getShots()) * 100); + int headshotAccuracy = stats.getShots() <= 0 + ? 100 + : (int)Math.rint( + ((double)(stats.getHeadshotHits()) / (double)stats.getShots()) * 100); + TagResolver[] tagResolvers = new TagResolver[] {Placeholder.component("map", settings.displayName()), + Placeholder.component("final_time", finalTime), roundPlaceholder, + Placeholder.component("total_shots", Component.text(stats.getShots())), + Placeholder.component("regular_hits", Component.text(stats.getRegularHits())), + Placeholder.component("headshot_hits", Component.text(stats.getHeadshotHits())), + Placeholder.component("gun_accuracy", Component.text(gunAccuracy)), + Placeholder.component("headshot_accuracy", Component.text(headshotAccuracy)), + Placeholder.component("kills", Component.text(stats.getKills())), + Placeholder.component("coins_gained", Component.text(stats.getCoinsGained())), + Placeholder.component("coins_spent", Component.text(stats.getCoinsSpent())), + Placeholder.component("knocks", Component.text(stats.getKnocks())), + Placeholder.component("deaths", Component.text(stats.getDeaths())), + Placeholder.component("revives", Component.text(stats.getRevives()))}; + + zombiesPlayer.sendMessage(MINI_MESSAGE.deserialize(settings.endGameStatsFormat(), tagResolvers)); + } + } + + private void runWebhook(String time) { + if (!webhook.enabled()) { + return; + } + + try { + List> futures = new ArrayList<>(zombiesPlayers.size()); + IntList kills = new IntArrayList(); + for (ZombiesPlayer player : zombiesPlayers) { + futures.add(player.module().getPlayerView().getUsername()); + kills.add(player.module().getKills().getKills()); + } + + String date = ""; + + CompletableFuture.allOf(futures.toArray(EMPTY_COMPLETABLE_FUTURE_ARRAY)).thenCompose(ignored -> { + List formattedUsernames = new ArrayList<>(futures.size()); + for (int i = 0; i < futures.size(); i++) { + formattedUsernames.add( + MessageFormat.format(webhook.playerFormat(), futures.get(i).join(), kills.getInt(i))); + } + + String playerList = String.join(", ", formattedUsernames); + String output = MessageFormat.format(webhook.webhookFormat(), date, time, playerList); + HttpRequest request = HttpRequest.newBuilder(URI.create(webhook.webhookURL())) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(output)) + .build(); + return client.sendAsync(request, HttpResponse.BodyHandlers.discarding()); + }).whenComplete((ignored, throwable) -> { + if (throwable != null) { + LOGGER.warn("Failed to send webhook", throwable); + } + }); + } + catch (Exception e) { + LOGGER.warn("Failed to send webhook", e); + } } @Override @@ -107,4 +271,8 @@ public void end() { public @NotNull Key key() { return StageKeys.END; } + + public boolean hasWon() { + return hasWon; + } } diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/IdleStage.java b/zombies/src/main/java/org/phantazm/zombies/stage/IdleStage.java index a9689a953..c7c662132 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/IdleStage.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/IdleStage.java @@ -1,7 +1,13 @@ package org.phantazm.zombies.stage; import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.phantazm.zombies.map.MapSettingsInfo; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.sidebar.SidebarUpdater; @@ -12,13 +18,27 @@ public class IdleStage implements Stage { private final Map sidebarUpdaters = new HashMap<>(); + private final Instance instance; + + private final MapSettingsInfo settings; + private final Collection zombiesPlayers; private final Function sidebarUpdaterCreator; - public IdleStage(@NotNull Collection zombiesPlayers, @NotNull Function sidebarUpdaterCreator) { + private final long revertTicks; + + private long emptyTicks; + + public IdleStage(@NotNull Instance instance, @NotNull MapSettingsInfo settings, + @NotNull Collection zombiesPlayers, + @NotNull Function sidebarUpdaterCreator, + long revertTicks) { + this.instance = Objects.requireNonNull(instance, "instance"); + this.settings = Objects.requireNonNull(settings, "settings"); this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); this.sidebarUpdaterCreator = Objects.requireNonNull(sidebarUpdaterCreator, "sidebarUpdaterCreator"); + this.revertTicks = revertTicks; } @Override @@ -32,8 +52,22 @@ public boolean shouldRevert() { } @Override - public void onJoin(@NotNull ZombiesPlayer zombiesPlayer) { + public boolean shouldAbort() { + return emptyTicks >= revertTicks; + } + @Override + public void onJoin(@NotNull ZombiesPlayer zombiesPlayer) { + zombiesPlayer.module().getLeaderboard().startIfNotActive(); + int count = zombiesPlayers.size(), maxPlayers = settings.maxPlayers(); + TagResolver countPlaceholder = Placeholder.component("count", Component.text(count)); + TagResolver maxPlayersPlaceholder = Placeholder.component("max_players", Component.text(maxPlayers)); + zombiesPlayer.module().getPlayerView().getDisplayName().thenAccept(displayName -> { + TagResolver joinerPlaceholder = Placeholder.component("joiner", displayName); + Component message = MiniMessage.miniMessage() + .deserialize(settings.gameJoinFormat(), joinerPlaceholder, countPlaceholder, maxPlayersPlaceholder); + instance.sendMessage(message); + }); } @Override @@ -42,16 +76,30 @@ public void onLeave(@NotNull ZombiesPlayer zombiesPlayer) { if (updater != null) { updater.end(); } + + zombiesPlayer.module().getLeaderboard().endIfActive(); + } + + @Override + public void start() { + emptyTicks = 0L; } @Override public void tick(long time) { - for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { - if (!zombiesPlayer.hasQuit()) { - SidebarUpdater sidebarUpdater = sidebarUpdaters.computeIfAbsent(zombiesPlayer.getUUID(), unused -> { - return sidebarUpdaterCreator.apply(zombiesPlayer); - }); - sidebarUpdater.tick(time); + if (zombiesPlayers.isEmpty()) { + ++emptyTicks; + } + else { + emptyTicks = 0L; + + for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { + if (!zombiesPlayer.hasQuit()) { + SidebarUpdater sidebarUpdater = sidebarUpdaters.computeIfAbsent(zombiesPlayer.getUUID(), unused -> { + return sidebarUpdaterCreator.apply(zombiesPlayer); + }); + sidebarUpdater.tick(time); + } } } } @@ -68,6 +116,11 @@ public boolean hasPermanentPlayers() { return false; } + @Override + public boolean canRejoin() { + return false; + } + @Override public @NotNull Key key() { return StageKeys.IDLE_STAGE; diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/InGameStage.java b/zombies/src/main/java/org/phantazm/zombies/stage/InGameStage.java index 5aa9a9c4b..8a760c3c4 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/InGameStage.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/InGameStage.java @@ -2,34 +2,20 @@ import com.github.steanky.toolkit.collection.Wrapper; import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; -import net.kyori.adventure.title.TitlePart; import net.minestom.server.coordinate.Pos; -import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.phantazm.core.equipment.EquipmentHandler; -import org.phantazm.core.time.TickFormatter; -import org.phantazm.zombies.map.MapSettingsInfo; import org.phantazm.zombies.map.handler.RoundHandler; import org.phantazm.zombies.map.handler.ShopHandler; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.ZombiesPlayerModule; import org.phantazm.zombies.sidebar.SidebarUpdater; -import org.phantazm.stats.zombies.ZombiesPlayerMapStats; import java.util.*; import java.util.function.Function; public class InGameStage implements Stage { - private final Instance instance; private final Collection zombiesPlayers; - private final MapSettingsInfo settings; private final Map sidebarUpdaters = new HashMap<>(); private final Pos spawnPos; private final RoundHandler roundHandler; @@ -37,19 +23,15 @@ public class InGameStage implements Stage { private final Map> defaultEquipment; private final Set equipmentGroups; private final Function sidebarUpdaterCreator; + private final ShopHandler shopHandler; - private final TickFormatter tickFormatter; - private final MiniMessage miniMessage = MiniMessage.miniMessage(); - public InGameStage(@NotNull Instance instance, @NotNull Collection zombiesPlayers, - @NotNull MapSettingsInfo settings, @NotNull Pos spawnPos, @NotNull RoundHandler roundHandler, - @NotNull Wrapper ticksSinceStart, @NotNull Map> defaultEquipment, - @NotNull Set equipmentGroups, + public InGameStage(@NotNull Collection zombiesPlayers, @NotNull Pos spawnPos, + @NotNull RoundHandler roundHandler, @NotNull Wrapper ticksSinceStart, + @NotNull Map> defaultEquipment, @NotNull Set equipmentGroups, @NotNull Function sidebarUpdaterCreator, - @NotNull ShopHandler shopHandler, @NotNull TickFormatter endTimeTickFormatter) { - this.instance = Objects.requireNonNull(instance, "instance"); + @NotNull ShopHandler shopHandler) { this.zombiesPlayers = Objects.requireNonNull(zombiesPlayers, "zombiesPlayers"); - this.settings = Objects.requireNonNull(settings, "settings"); this.spawnPos = Objects.requireNonNull(spawnPos, "spawnPos"); this.roundHandler = Objects.requireNonNull(roundHandler, "roundHandler"); this.ticksSinceStart = Objects.requireNonNull(ticksSinceStart, "ticksSinceStart"); @@ -57,7 +39,10 @@ public InGameStage(@NotNull Instance instance, @NotNull Collection { - if (ticksSinceStart.get() < prevBest) { - stats.setBestTime(ticksSinceStart.get()); - } - }, () -> stats.setBestTime(ticksSinceStart.get())); - } - } - else { - instance.sendTitlePart(TitlePart.TITLE, miniMessage.deserialize(settings.lossTitleFormat(), roundPlaceholder)); - instance.sendTitlePart(TitlePart.SUBTITLE, miniMessage.deserialize(settings.lossSubtitleFormat(), roundPlaceholder)); - } - - for (ZombiesPlayer zombiesPlayer : zombiesPlayers) { - if (zombiesPlayer.hasQuit()) { - continue; - } - - ZombiesPlayerMapStats stats = zombiesPlayer.module().getStats(); - int gunAccuracy = stats.getShots() <= 0 - ? 100 - : (int)Math.rint(((double)(stats.getRegularHits() + stats.getHeadshotHits()) / - (double)stats.getShots()) * 100); - int headshotAccuracy = stats.getShots() <= 0 - ? 100 - : (int)Math.rint(((double)(stats.getHeadshotHits()) / (double)stats.getShots()) * 100); - TagResolver[] tagResolvers = new TagResolver[] { - Placeholder.component("map", settings.displayName()), - Placeholder.component("final_time", finalTime), - Placeholder.component("best_round", Component.text(bestRound)), - Placeholder.component("total_shots", Component.text(stats.getShots())), - Placeholder.component("regular_hits", Component.text(stats.getRegularHits())), - Placeholder.component("headshot_hits", Component.text(stats.getHeadshotHits())), - Placeholder.component("gun_accuracy", Component.text(gunAccuracy)), - Placeholder.component("headshot_accuracy", Component.text(headshotAccuracy)), - Placeholder.component("kills", Component.text(stats.getKills())), - Placeholder.component("coins_gained", Component.text(stats.getCoinsGained())), - Placeholder.component("coins_spent", Component.text(stats.getCoinsSpent())), - Placeholder.component("knocks", Component.text(stats.getKnocks())), - Placeholder.component("deaths", Component.text(stats.getDeaths())), - Placeholder.component("revives", Component.text(stats.getRevives())) - }; - - zombiesPlayer.sendMessage(miniMessage.deserialize(settings.endGameStatsFormat(), tagResolvers)); - } - for (SidebarUpdater sidebarUpdater : sidebarUpdaters.values()) { sidebarUpdater.end(); } + + roundHandler.end(); } @Override diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/Stage.java b/zombies/src/main/java/org/phantazm/zombies/stage/Stage.java index a1171c37d..461f26fad 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/Stage.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/Stage.java @@ -1,28 +1,24 @@ package org.phantazm.zombies.stage; -import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; import org.jetbrains.annotations.NotNull; import org.phantazm.commons.Activable; import org.phantazm.zombies.player.ZombiesPlayer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; - public interface Stage extends Activable, Keyed { boolean shouldContinue(); boolean shouldRevert(); + boolean shouldAbort(); + void onJoin(@NotNull ZombiesPlayer zombiesPlayer); void onLeave(@NotNull ZombiesPlayer zombiesPlayer); boolean hasPermanentPlayers(); + boolean canRejoin(); + } diff --git a/zombies/src/main/java/org/phantazm/zombies/stage/StageTransition.java b/zombies/src/main/java/org/phantazm/zombies/stage/StageTransition.java index 5244fa391..3ba06460f 100644 --- a/zombies/src/main/java/org/phantazm/zombies/stage/StageTransition.java +++ b/zombies/src/main/java/org/phantazm/zombies/stage/StageTransition.java @@ -2,9 +2,10 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; +import org.phantazm.commons.Activable; import org.phantazm.commons.Tickable; -public class StageTransition implements Tickable { +public class StageTransition implements Activable { private final Stage[] stages; @@ -14,8 +15,10 @@ public class StageTransition implements Tickable { public StageTransition(@NotNull Stage @NotNull ... stages) { this.stages = stages.clone(); - this.currentStageIndex = 0; - this.currentStage = stages[currentStageIndex]; + } + + public void start() { + setCurrentStageIndex(0); } @Override @@ -32,6 +35,10 @@ public void tick(long time) { setCurrentStageIndex(currentStageIndex - 1); return; } + if (currentStage.shouldAbort()) { + setCurrentStageIndex(stages.length); + return; + } currentStage.tick(time); } @@ -55,7 +62,9 @@ private void setCurrentStageIndex(int currentStageIndex) { } this.currentStageIndex = currentStageIndex; - currentStage.end(); + if (currentStage != null) { + currentStage.end(); + } if (currentStageIndex != stages.length) { currentStage = stages[currentStageIndex]; @@ -69,4 +78,10 @@ private void setCurrentStageIndex(int currentStageIndex) { public Stage getCurrentStage() { return currentStage; } + + public void end() { + if (currentStageIndex != stages.length) { + setCurrentStageIndex(stages.length); + } + } } diff --git a/zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIterationTest.java b/zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIterationTest.java similarity index 59% rename from zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIterationTest.java rename to zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIterationTest.java index b8b8570bb..63898ec4b 100644 --- a/zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshotBlockIterationTest.java +++ b/zombies/src/test/java/org/phantazm/zombies/equipment/gun/shoot/blockiteration/WallshootingBlockIterationTest.java @@ -3,6 +3,7 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; +import org.phantazm.zombies.equipment.gun.shoot.wallshooting.WallshootingChecker; import java.util.Collections; import java.util.Set; @@ -10,11 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -public class WallshotBlockIterationTest { +public class WallshootingBlockIterationTest { @Test public void testAirDoesNotEndIteration() { - BlockIteration blockIteration = new WallshotBlockIteration(new WallshotBlockIteration.Data(Set.of())); + WallshootingChecker wallshootingChecker = () -> true; + BlockIteration blockIteration = + new WallshootingBlockIteration(new WallshootingBlockIteration.Data("", Set.of()), wallshootingChecker); BlockIteration.Context context = blockIteration.createContext(); assertFalse(context.acceptRaytracedBlock(Vec.ZERO, Block.AIR)); @@ -22,7 +25,9 @@ public void testAirDoesNotEndIteration() { @Test public void testStoneIsInitiallyValidEndpoint() { - BlockIteration blockIteration = new WallshotBlockIteration(new WallshotBlockIteration.Data(Set.of())); + WallshootingChecker wallshootingChecker = () -> true; + BlockIteration blockIteration = + new WallshootingBlockIteration(new WallshootingBlockIteration.Data("", Set.of()), wallshootingChecker); BlockIteration.Context context = blockIteration.createContext(); assertTrue(context.isValidEndpoint(Vec.ZERO, Block.STONE)); @@ -31,8 +36,9 @@ public void testStoneIsInitiallyValidEndpoint() { @Test public void testStoneIsInvalidEndpointAfterWallshot() { Block passable = Block.BARRIER; - BlockIteration blockIteration = - new WallshotBlockIteration(new WallshotBlockIteration.Data(Collections.singleton(passable.key()))); + WallshootingChecker wallshootingChecker = () -> true; + BlockIteration blockIteration = new WallshootingBlockIteration( + new WallshootingBlockIteration.Data("", Collections.singleton(passable.key())), wallshootingChecker); BlockIteration.Context context = blockIteration.createContext(); context.acceptRaytracedBlock(Vec.ZERO, passable); diff --git a/zombies/src/test/java/org/phantazm/zombies/game/coin/BasicPlayerCoinsTest.java b/zombies/src/test/java/org/phantazm/zombies/game/coin/BasicPlayerCoinsTest.java index 99ff065f1..28a31ce79 100644 --- a/zombies/src/test/java/org/phantazm/zombies/game/coin/BasicPlayerCoinsTest.java +++ b/zombies/src/test/java/org/phantazm/zombies/game/coin/BasicPlayerCoinsTest.java @@ -2,18 +2,21 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.phantazm.core.player.PlayerView; import org.phantazm.stats.zombies.BasicZombiesPlayerMapStats; -import org.phantazm.stats.zombies.ZombiesPlayerMapStats; import org.phantazm.zombies.coin.BasicPlayerCoins; import org.phantazm.zombies.coin.PlayerCoins; import org.phantazm.zombies.coin.Transaction; import org.phantazm.zombies.coin.TransactionResult; -import org.phantazm.zombies.coin.component.BasicTransactionComponentCreator; -import org.phantazm.zombies.coin.component.TransactionComponentCreator; +import org.phantazm.zombies.coin.component.BasicTransactionMessager; +import org.phantazm.zombies.coin.component.TransactionMessager; +import org.phantazm.zombies.map.PlayerCoinsInfo; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import java.util.Collections; import java.util.List; @@ -32,9 +35,12 @@ private void setup(int initialCoins) { PlayerView playerView = mock(PlayerView.class); Player player = mock(Player.class); when(playerView.getPlayer()).thenReturn(Optional.of(player)); - TransactionComponentCreator componentCreator = new BasicTransactionComponentCreator(); + ZombiesPlayerActionBar actionBar = new ZombiesPlayerActionBar(playerView); + TransactionMessager componentCreator = + new BasicTransactionMessager(actionBar, MiniMessage.miniMessage(), new PlayerCoinsInfo("", "", + NamedTextColor.WHITE, NamedTextColor.WHITE, 20L)); - coins = new BasicPlayerCoins(playerView, + coins = new BasicPlayerCoins( BasicZombiesPlayerMapStats.createBasicStats(UUID.randomUUID(), Key.key("phantazm:test")), componentCreator, initialCoins); } @@ -124,7 +130,8 @@ public int getPriority() { return 1; } }; - TransactionResult result = coins.runTransaction(new Transaction(List.of(modifier1, modifier2), delta)); + TransactionResult result = + coins.runTransaction(new Transaction(List.of(modifier1, modifier2), delta)); assertEquals(changedValue + delta, result.change()); } diff --git a/zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerTest.java b/zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerIntegrationTest.java similarity index 53% rename from zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerTest.java rename to zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerIntegrationTest.java index ec5bfd6da..a16c7d18d 100644 --- a/zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerTest.java +++ b/zombies/src/test/java/org/phantazm/zombies/game/player/state/revive/ReviveHandlerIntegrationTest.java @@ -1,19 +1,33 @@ package org.phantazm.zombies.game.player.state.revive; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.phantazm.core.player.PlayerView; import org.phantazm.stats.zombies.ZombiesPlayerMapStats; import org.phantazm.zombies.player.ZombiesPlayer; import org.phantazm.zombies.player.ZombiesPlayerMeta; import org.phantazm.zombies.player.ZombiesPlayerModule; +import org.phantazm.zombies.player.action_bar.ZombiesPlayerActionBar; import org.phantazm.zombies.player.state.ZombiesPlayerState; +import org.phantazm.zombies.player.state.context.KnockedPlayerStateContext; import org.phantazm.zombies.player.state.revive.ReviveHandler; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -public class ReviveHandlerTest { +@EnvTest +public class ReviveHandlerIntegrationTest { + + private Collection zombiesPlayers; private ZombiesPlayerState aliveState; @@ -31,15 +45,23 @@ public void setup() { ZombiesPlayerModule module = mock(ZombiesPlayerModule.class); when(module.getMeta()).thenReturn(meta); when(module.getStats()).thenReturn(mock(ZombiesPlayerMapStats.class)); + PlayerView playerView = mock(PlayerView.class); + when(playerView.getPlayer()).thenReturn(Optional.empty()); + when(module.getPlayerView()).thenReturn(playerView); + ZombiesPlayerActionBar actionBar = new ZombiesPlayerActionBar(playerView); + when(module.getActionBar()).thenReturn(actionBar); reviver = mock(ZombiesPlayer.class); when(reviver.module()).thenReturn(module); + zombiesPlayers = Collections.singleton(reviver); } @Test - public void testDeathContinuesWithoutReviver() { + public void testDeathContinuesWithoutReviver(Env env) { + KnockedPlayerStateContext context = new KnockedPlayerStateContext(env.createFlatInstance(), Vec.ZERO, null, null); long initialDeathTime = 20L; ReviveHandler reviveHandler = - new ReviveHandler(() -> aliveState, () -> deathState, () -> null, initialDeathTime); + new ReviveHandler(context, zombiesPlayers, ignored -> aliveState, () -> deathState, ignored -> false, + initialDeathTime); reviveHandler.tick(0L); @@ -49,12 +71,14 @@ public void testDeathContinuesWithoutReviver() { } @Test - public void testDeathStopsWithReviverAndReviverSet() { + public void testDeathStopsWithReviverAndReviverSet(Env env) { + KnockedPlayerStateContext context = new KnockedPlayerStateContext(env.createFlatInstance(), Vec.ZERO, null, null); long reviveTime = 10L; when(reviver.getReviveTime()).thenReturn(reviveTime); long initialDeathTime = 20L; ReviveHandler reviveHandler = - new ReviveHandler(() -> aliveState, () -> deathState, () -> reviver, initialDeathTime); + new ReviveHandler(context, zombiesPlayers, ignored -> aliveState, () -> deathState, + candidate -> candidate == reviver, initialDeathTime); reviveHandler.tick(0L); @@ -65,10 +89,12 @@ public void testDeathStopsWithReviverAndReviverSet() { } @Test - public void testDeathSuggestsDeathState() { + public void testDeathSuggestsDeathState(Env env) { + KnockedPlayerStateContext context = new KnockedPlayerStateContext(env.createFlatInstance(), Vec.ZERO, null, null); long initialDeathTime = 0L; ReviveHandler reviveHandler = - new ReviveHandler(() -> aliveState, () -> deathState, () -> null, initialDeathTime); + new ReviveHandler(context, zombiesPlayers, ignored -> aliveState, () -> deathState, ignored -> false, + initialDeathTime); reviveHandler.tick(0L); @@ -77,10 +103,13 @@ public void testDeathSuggestsDeathState() { } @Test - public void testReviveSuggestsAliveState() { + public void testReviveSuggestsAliveState(Env env) { + KnockedPlayerStateContext context = new KnockedPlayerStateContext(env.createFlatInstance(), Vec.ZERO, null, null); long reviveTime = 0L; when(reviver.getReviveTime()).thenReturn(reviveTime); - ReviveHandler reviveHandler = new ReviveHandler(() -> aliveState, () -> deathState, () -> reviver, 20L); + ReviveHandler reviveHandler = + new ReviveHandler(context, zombiesPlayers, ignored -> aliveState, () -> deathState, + candidate -> candidate == reviver, 20L); reviveHandler.tick(0L); reviveHandler.tick(0L); @@ -89,20 +118,4 @@ public void testReviveSuggestsAliveState() { Assertions.assertEquals(aliveState, reviveHandler.getSuggestedState().get()); } - @Test - public void testDisabledReviverResetsRevive() { - when(reviver.getReviveTime()).thenReturn(10L); - long initialDeathTime = 20L; - ReviveHandler reviveHandler = - new ReviveHandler(() -> aliveState, () -> deathState, () -> reviver, initialDeathTime); - - reviveHandler.tick(0L); - reviveHandler.tick(0L); - - assertFalse(reviveHandler.isReviving()); - verify(meta).setReviving(false); - assertEquals(initialDeathTime, reviveHandler.getTicksUntilDeath()); - assertEquals(-1, reviveHandler.getTicksUntilRevive()); - } - }