diff --git a/core/src/main/java/eu/mhsl/craftattack/spawn/core/Main.java b/core/src/main/java/eu/mhsl/craftattack/spawn/core/Main.java index e666a90..a0ace2f 100644 --- a/core/src/main/java/eu/mhsl/craftattack/spawn/core/Main.java +++ b/core/src/main/java/eu/mhsl/craftattack/spawn/core/Main.java @@ -10,6 +10,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.reflections.Reflections; import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -49,7 +50,7 @@ public final class Main extends JavaPlugin { Main.logger().info(String.format("Loaded %d repositories!", this.repositoryLoader.getRepositories().size())); Main.logger().info("Loading appliances..."); - this.appliances = this.findSubtypesOf(Appliance.class).stream() + this.appliances = new ArrayList<>(this.findSubtypesOf(Appliance.class).stream() .filter(applianceClass -> !disabledAppliances.contains(applianceClass.getSimpleName())) .filter(appliance -> { Appliance.Flags flags = appliance.getAnnotation(Appliance.Flags.class); @@ -63,14 +64,17 @@ public final class Main extends JavaPlugin { throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e); } }) - .toList(); + .toList()); Main.logger().info(String.format("Loaded %d appliances!", this.appliances.size())); Main.logger().info("Initializing appliances..."); - this.appliances.forEach(appliance -> { - appliance.onEnable(); - appliance.initialize(this); - }); + this.appliances.stream() + .filter(appliance -> { + Appliance.Flags flags = appliance.getClass().getAnnotation(Appliance.Flags.class); + if(flags == null) return true; + return flags.autoload(); + }) + .forEach(appliance -> appliance.enableSequence(this)); Main.logger().info(String.format("Initialized %d appliances!", this.appliances.size())); if(Configuration.pluginConfig.getBoolean("httpServerEnabled", true)) { @@ -85,17 +89,34 @@ public final class Main extends JavaPlugin { @Override public void onDisable() { Main.logger().info("Disabling appliances..."); - this.appliances.forEach(appliance -> { - Main.logger().info("Disabling " + appliance.getClass().getSimpleName()); - appliance.onDisable(); - appliance.destruct(this); - }); + this.appliances.forEach(appliance -> appliance.disableSequence(this)); HandlerList.unregisterAll(this); Bukkit.getScheduler().cancelTasks(this); Main.logger().info("Disabled " + this.appliances.size() + " appliances!"); } + public Appliance restartAppliance(Class applianceClass) { + Appliance oldAppliance = this.appliances.stream() + .filter(appliance -> appliance.getClass().equals(applianceClass)) + .findFirst() + .orElseThrow(); + oldAppliance.disableSequence(this); + this.appliances.remove(oldAppliance); + Appliance newAppliance = this.createApplianceInstance(applianceClass); + this.appliances.add(newAppliance); + newAppliance.enableSequence(this); + return newAppliance; + } + + private Appliance createApplianceInstance(Class applianceClass) { + try { + return applianceClass.getDeclaredConstructor().newInstance(); + } catch(Exception e) { + throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e); + } + } + public T getAppliance(Class clazz) { return this.appliances.stream() .filter(clazz::isInstance) diff --git a/core/src/main/java/eu/mhsl/craftattack/spawn/core/appliance/Appliance.java b/core/src/main/java/eu/mhsl/craftattack/spawn/core/appliance/Appliance.java index 9650759..f59454b 100644 --- a/core/src/main/java/eu/mhsl/craftattack/spawn/core/appliance/Appliance.java +++ b/core/src/main/java/eu/mhsl/craftattack/spawn/core/appliance/Appliance.java @@ -28,6 +28,7 @@ public abstract class Appliance { @Retention(RetentionPolicy.RUNTIME) public @interface Flags { boolean enabled() default true; + boolean autoload() default true; } private String localConfigPath; @@ -101,9 +102,20 @@ public abstract class Appliance { } public void destruct(@NotNull JavaPlugin plugin) { + if(this.listeners == null) return; this.listeners.forEach(HandlerList::unregisterAll); } + public void enableSequence(JavaPlugin plugin) { + this.onEnable(); + this.initialize(plugin); + } + + public void disableSequence(JavaPlugin plugin) { + this.onDisable(); + this.destruct(plugin); + } + protected T queryAppliance(Class clazz) { return Main.instance().getAppliance(clazz); } diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/bloodmoon/Bloodmoon.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/bloodmoon/Bloodmoon.java index b4de47f..6cbaf46 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/bloodmoon/Bloodmoon.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/bloodmoon/Bloodmoon.java @@ -92,10 +92,10 @@ public class Bloodmoon extends Appliance { private final Map hordeSpawnTasks = new WeakHashMap<>(); private long lastBloodmoonStartTick = 0; public final int ticksPerDay = 24000; - public final int bloodmoonLength = ticksPerDay/2; + public final int bloodmoonLength = this.ticksPerDay /2; public final int preStartMessageTicks = Ticks.TICKS_PER_SECOND * 50; private final int bloodmoonFreeDaysAtStart = 3; - private final int bloodmoonStartTime = ticksPerDay/2; + private final int bloodmoonStartTime = this.ticksPerDay /2; private final int bloodmoonDayInterval = 30; private final int minBonusDrops = 1; private final int maxBonusDrops = 4; diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/Event.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/Event.java index 01853fd..8fa5679 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/Event.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/Event.java @@ -39,6 +39,11 @@ public class Event extends Appliance { DONE } + public enum EventType { + BIG, + SMALL + } + Countdown advertiseCountdown = new Countdown( 120, announcementData -> Component.text() @@ -52,6 +57,7 @@ public class Event extends Appliance { ); public DisplayVillager.ConfigBound villager; private boolean isOpen = false; + private EventType eventType; private AdvertisementStatus advertiseStatus = AdvertisementStatus.BEFORE; private UUID roomId; private final List pendingRewards = new ArrayList<>(); @@ -81,18 +87,24 @@ public class Event extends Appliance { if(this.isOpen) this.roomId = UUID.fromString(this.localConfig().getString("roomId", "")); } - public void openEvent() { + public void openEvent(EventType type) { if(this.isOpen) throw new ApplianceCommand.Error("Es läuft derzeit bereits ein Event!"); + this.eventType = type; - Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { - ReqResp sessionResponse = this.queryRepository(EventRepository.class).createSession(); + if(type.equals(EventType.SMALL)) { + Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { + ReqResp sessionResponse = this.queryRepository(EventRepository.class).createSession(); - if(sessionResponse.status() != HttpStatus.OK) - throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status()); + if(sessionResponse.status() != HttpStatus.OK) + throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status()); - this.isOpen = true; - this.roomId = sessionResponse.data().uuid(); - }); + this.isOpen = true; + this.roomId = sessionResponse.data().uuid(); + }); + return; + } + + this.isOpen = true; } public void joinEvent(Player p) { @@ -135,18 +147,22 @@ public class Event extends Appliance { Main.instance().getLogger().info("Verbinde mit eventserver: " + p.getName()); p.sendMessage(Component.text("Authentifiziere...", NamedTextColor.GREEN)); - Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { - ReqResp queueResponse = this.queryRepository(EventRepository.class) - .queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId)); + if(this.eventType.equals(EventType.SMALL)) { + Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { + ReqResp queueResponse = this.queryRepository(EventRepository.class) + .queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId)); - if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) { - p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED)); - return; - } + if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) { + p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED)); + return; + } - p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN)); - PluginMessage.connect(p, this.localConfig().getString("connect-server-name")); - }); + p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN)); + PluginMessage.connect(p, this.localConfig().getString("connect-server-name")); + }); + } + + PluginMessage.connect(p, "grand-event"); } public void endEvent() { diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/command/EventOpenSessionCommand.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/command/EventOpenSessionCommand.java index e633640..1bf11fe 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/command/EventOpenSessionCommand.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/event/command/EventOpenSessionCommand.java @@ -7,6 +7,10 @@ import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; public class EventOpenSessionCommand extends ApplianceCommand { public EventOpenSessionCommand() { @@ -15,7 +19,17 @@ public class EventOpenSessionCommand extends ApplianceCommand { @Override protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { - this.getAppliance().openEvent(); + if(args.length == 1) { + this.getAppliance().openEvent(Event.EventType.SMALL); + } else { + this.getAppliance().openEvent(Event.EventType.valueOf(args[1])); + } sender.sendMessage(Component.text("Event-Server gestartet!", NamedTextColor.GREEN)); } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if(args.length == 1) return Arrays.stream(Event.EventType.values()).map(Enum::toString).toList(); + return null; + } } diff --git a/event/build.gradle b/event/build.gradle new file mode 100644 index 0000000..fa35910 --- /dev/null +++ b/event/build.gradle @@ -0,0 +1,9 @@ +dependencies { + implementation project(':common') + implementation project(':core') + + compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT' + compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' + implementation 'org.apache.httpcomponents:httpclient:4.5.14' + implementation 'com.sparkjava:spark-core:2.9.4' +} \ No newline at end of file diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/Deathrun.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/Deathrun.java new file mode 100644 index 0000000..bebb36f --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/Deathrun.java @@ -0,0 +1,104 @@ +package eu.mhsl.craftattack.spawn.event.appliances.deathrun; + +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.Event; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.Scorable; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard.EventScoreboardBuilder; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.Bukkit; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@Appliance.Flags(autoload = false) +public class Deathrun extends Appliance implements Event, Scorable { + private final EventScoreboardBuilder scoreboardBuilder = new EventScoreboardBuilder(this, 3, 2, 3); + private final double borderDistance = 100; + private final int borderVisibilityDistance = 8; + private long durationSeconds; + + @Override + public void onEnable() { + World world = Bukkit.getWorlds().getFirst(); + world.getWorldBorder().setCenter(world.getSpawnLocation()); + world.getWorldBorder().setSize(20); + } + + public double getBorderDistance() { + return this.borderDistance; + } + + public int getBorderVisibilityDistance() { + return this.borderVisibilityDistance; + } + + public void spawnParticleWall(Player p, double xMin, double xMax, double yMin, double yMax, double zMin, double zMax) { + Particle particle = Particle.WAX_ON; + + for (double y = yMin; y <= yMax; y += 0.5) { + for (double z = zMin; z <= zMax; z += 0.5) { + for (double x = xMin; x <= xMax; x += 0.5) { + p.spawnParticle(particle, x, y, z, 1, 0.05, 0.05, 0.05, 0); + } + } + } + } + + @Override + public void onDisable() { + World world = Bukkit.getWorlds().getFirst(); + world.getWorldBorder().setSize(world.getWorldBorder().getMaxSize()); + } + + @Override + public void start(long durationSeconds) { + this.durationSeconds = durationSeconds; + Title title = Title.title(Component.text("Start"), Component.text("Laufe Richtung Osten! (positiv x)")); + Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title)); + + World world = Bukkit.getWorlds().getFirst(); + world.getWorldBorder().setSize(world.getWorldBorder().getMaxSize()); + } + + @Override + public int getScore(Player p) { + return (int) Math.ceil(p.getLocation().x()); + } + + @Override + public void stop() { + this.getScoreboardBuilder().stopAutomaticUpdates(); + Title title = Title.title(Component.text("Ende!"), Component.empty()); + Bukkit.getOnlinePlayers().forEach(player -> { + player.showTitle(title); + player.setScoreboard(Bukkit.getScoreboardManager().getNewScoreboard()); + }); + } + + @Override + public long getDurationSeconds() { + return this.durationSeconds; + } + + @Override + public String getScoreboardName() { + return "Deathrun"; + } + + @Override + public EventScoreboardBuilder getScoreboardBuilder() { + return this.scoreboardBuilder; + } + + @Override + protected @NotNull List listeners() { + return List.of( + new DeathrunPlayerMoveListener() + ); + } +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/DeathrunPlayerMoveListener.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/DeathrunPlayerMoveListener.java new file mode 100644 index 0000000..e65ab71 --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/deathrun/DeathrunPlayerMoveListener.java @@ -0,0 +1,35 @@ +package eu.mhsl.craftattack.spawn.event.appliances.deathrun; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerMoveEvent; + +public class DeathrunPlayerMoveListener extends ApplianceListener { + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + Location spawnLocation = Bukkit.getWorlds().getFirst().getSpawnLocation(); + double minX = spawnLocation.x() - this.getAppliance().getBorderDistance(); + double minZ = spawnLocation.z() - this.getAppliance().getBorderDistance(); + double maxZ = spawnLocation.z() + this.getAppliance().getBorderDistance(); + if(event.getTo().x() < minX + this.getAppliance().getBorderVisibilityDistance()) { + this.getAppliance().spawnParticleWall(event.getPlayer(), minX-0.2, minX-0.2, event.getTo().y()-0.5, event.getTo().y()+2.5, event.getTo().z()-1.5, event.getTo().z()+1.5); + if(event.getTo().x() < minX) { + event.setTo(event.getTo().clone().set(minX, event.getTo().y(), event.getTo().z())); + } + } + if(event.getTo().z() < minZ + this.getAppliance().getBorderVisibilityDistance()) { + this.getAppliance().spawnParticleWall(event.getPlayer(), event.getTo().x()-1.5, event.getTo().x()+1.5, event.getTo().y()-0.5, event.getTo().y()+2.5, minZ-0.2, minZ-0.2); + if(event.getTo().z() < minZ) { + event.setTo(event.getTo().clone().set(event.getTo().x(), event.getTo().y(), minZ)); + } + } + if(event.getTo().z() > maxZ - this.getAppliance().getBorderVisibilityDistance()) { + this.getAppliance().spawnParticleWall(event.getPlayer(), event.getTo().x()-1.5, event.getTo().x()+1.5, event.getTo().y()-0.5, event.getTo().y()+2.5, maxZ+0.2, maxZ+0.2); + if(event.getTo().z() > maxZ) { + event.setTo(event.getTo().clone().set(event.getTo().x(), event.getTo().y(), maxZ)); + } + } + } +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Event.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Event.java new file mode 100644 index 0000000..59af224 --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Event.java @@ -0,0 +1,7 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController; + +public interface Event { + void start(long durationSeconds); + void stop(); + long getDurationSeconds(); +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/EventController.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/EventController.java new file mode 100644 index 0000000..ecc4019 --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/EventController.java @@ -0,0 +1,101 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController; + +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.commands.BigEventCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.util.Ticks; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class EventController extends Appliance { + private List eventAppliances = null; + private Event selectedEvent = null; + private long timerStart; + private int timerTaskId = -1; + + @Override + public void onEnable() { + this.eventAppliances = Main.instance().getAppliances().stream() + .filter(appliance -> appliance instanceof Event) + .toList(); + } + + public List getEventAppliances() { + if(this.eventAppliances == null) throw new IllegalStateException("Event appliances were not initialized"); + return this.eventAppliances; + } + + public void loadEvent(Appliance appliance) { + if(!(appliance instanceof Event)) throw new IllegalArgumentException("Appliance has to implement Event."); + this.unloadEvent(); + Appliance newAppliance = Main.instance().restartAppliance(appliance.getClass()); + if(!(newAppliance instanceof Event newEvent)) throw new IllegalArgumentException("Appliance has to implement Event."); + this.selectedEvent = newEvent; + } + + public void unloadEvent() { + if(this.selectedEvent == null) return; + this.selectedEvent.stop(); + if(this.selectedEvent instanceof Appliance appliance) { + appliance.disableSequence(Main.instance()); + } + this.selectedEvent = null; + } + + public void startEvent(long durationMinutes) { + if(this.selectedEvent == null) throw new ApplianceCommand.Error("There is no event selected!"); + this.selectedEvent.start(durationMinutes * 60); + if(this.selectedEvent instanceof Scorable scorable && scorable.automaticUpdates()) scorable.getScoreboardBuilder().startAutomaticUpdates(); + // TODO: possibility for other dimensions + this.timerStart = Bukkit.getWorlds().getFirst().getFullTime(); + this.timerTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask( + Main.instance(), + this::updateTimer, + Ticks.TICKS_PER_SECOND, + Ticks.TICKS_PER_SECOND + ); + } + + public void stopEvent() { + if(this.selectedEvent == null) throw new ApplianceCommand.Error("There is no event selected!"); + this.selectedEvent.stop(); + if(this.timerTaskId != -1) Bukkit.getScheduler().cancelTask(this.timerTaskId); + this.timerTaskId = -1; + } + + public String getSelectedEvent() { + if(this.selectedEvent == null) return "nothing selected"; + return this.selectedEvent.getClass().getSimpleName().toLowerCase(); + } + + private void updateTimer() { + long ticksLeft = this.timerStart - (Bukkit.getWorlds().getFirst().getFullTime() - this.selectedEvent.getDurationSeconds() * Ticks.TICKS_PER_SECOND); + if(ticksLeft <= 0) { + if(this.timerTaskId != -1) Bukkit.getScheduler().cancelTask(this.timerTaskId); + this.timerTaskId = -1; + this.selectedEvent.stop(); + Bukkit.getOnlinePlayers().forEach(player -> player.sendActionBar(Component.text("Fertig!"))); + return; + } + + Bukkit.getOnlinePlayers().forEach(player -> player.sendActionBar( + Component.text(formatSeconds(ticksLeft / Ticks.TICKS_PER_SECOND), NamedTextColor.GOLD) + )); + } + + public static String formatSeconds(long seconds){ + return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60); + } + + @Override + protected @NotNull List> commands() { + return List.of( + new BigEventCommand() + ); + } +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Scorable.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Scorable.java new file mode 100644 index 0000000..dab46cf --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/Scorable.java @@ -0,0 +1,13 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController; + +import eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard.EventScoreboardBuilder; +import org.bukkit.entity.Player; + +public interface Scorable { + String getScoreboardName(); + default boolean automaticUpdates() { + return true; + } + EventScoreboardBuilder getScoreboardBuilder(); + int getScore(Player p); +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/commands/BigEventCommand.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/commands/BigEventCommand.java new file mode 100644 index 0000000..21d67ae --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/commands/BigEventCommand.java @@ -0,0 +1,75 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController.commands; + +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.EventController; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class BigEventCommand extends ApplianceCommand { + public BigEventCommand() { + super("event"); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if(args.length == 0) throw new Error("No argument selected!"); + + switch(args[0]) { + case "unload": { + this.getAppliance().unloadEvent(); + break; + } + case "load": { + if(args.length == 1) throw new Error("Not enough arguments for select."); + this.getAppliance().loadEvent(this.findApplianceFromString(args[1])); + break; + } + case "selected": { + sender.sendMessage("This Event is currently selected: %s".formatted(this.getAppliance().getSelectedEvent())); + break; + } + case "start": { + if(args.length == 1) { + this.getAppliance().startEvent(60 * 2); + break; + } + if(args.length == 2) { + try { + this.getAppliance().startEvent(Long.parseLong(args[1])); + } catch(NumberFormatException e) { + throw new Error("Last argument has to be a long."); + } + } + break; + } + case "stop": { + this.getAppliance().stopEvent(); + break; + } + default: throw new Error("No such option: '%s' !".formatted(args[0])); + } + } + + private Appliance findApplianceFromString(String name) { + System.out.println(this.getAppliance().getEventAppliances()); + return this.getAppliance().getEventAppliances().stream() + .filter(appliance -> appliance.getClass().getSimpleName().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new Error("Event appliance '%s' not found.".formatted(name))); + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if(args.length == 1) return List.of("load", "unload", "selected", "start", "stop"); + if(args.length == 2 && args[0].equals("load")) return this.getAppliance().getEventAppliances().stream() + .map(appliance -> appliance.getClass().getSimpleName().toLowerCase()) + .toList(); + if(args.length == 2 && args[0].equals("start")) return List.of(""); + return null; + } +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreEntry.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreEntry.java new file mode 100644 index 0000000..3e78218 --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreEntry.java @@ -0,0 +1,6 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard; + +import java.util.UUID; + +public record EventScoreEntry(UUID playerUuid, String name, int score) { +} diff --git a/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreboardBuilder.java b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreboardBuilder.java new file mode 100644 index 0000000..282deb8 --- /dev/null +++ b/event/src/main/java/eu/mhsl/craftattack/spawn/event/appliances/eventController/scoreboard/EventScoreboardBuilder.java @@ -0,0 +1,147 @@ +package eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard; + +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.event.appliances.eventController.Scorable; +import io.papermc.paper.scoreboard.numbers.NumberFormat; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.util.Ticks; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.DisplaySlot; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; + +import java.util.*; +import java.util.stream.IntStream; + +public class EventScoreboardBuilder { + private final int topPlaces; + private final int aroundPlaces; + private final int bottomPlaces; + private final Scorable scorable; + private final Comparator scoreComparator; + private final List playerScores = new ArrayList<>(); + private int scoreboardUpdateTaskId = -1; + + public EventScoreboardBuilder(Scorable scorable, int topPlaces, int aroundPlaces, int bottomPlaces) { + this(scorable, topPlaces, aroundPlaces, bottomPlaces, Comparator.comparingInt(EventScoreEntry::score).reversed()); + } + + public EventScoreboardBuilder(Scorable scorable, int topPlaces, int aroundPlaces, int bottomPlaces, Comparator scoreComparator) { + this.scorable = scorable; + this.topPlaces = topPlaces; + this.aroundPlaces = aroundPlaces; + this.bottomPlaces = bottomPlaces; + this.scoreComparator = scoreComparator; + } + + public Scoreboard buildFor(Player p) { + List scoreList = new ArrayList<>(this.playerScores); + + Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + Objective objective = scoreboard.registerNewObjective( + "event", "dummy", + Component.text( + "Scoreboard %s".formatted(this.scorable.getScoreboardName()), + NamedTextColor.GOLD, TextDecoration.BOLD + ) + ); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + objective.numberFormat(NumberFormat.blank()); + + UUID uuid = p.getUniqueId(); + scoreList.removeIf(e -> e.playerUuid().equals(uuid)); + scoreList.add(new EventScoreEntry(uuid, p.getName(), this.scorable.getScore(p))); + + scoreList.sort(this.scoreComparator); + + int size = scoreList.size(); + int playerIndex = IntStream.range(0, size) + .filter(i -> scoreList.get(i).playerUuid().equals(uuid)) + .findFirst() + .orElse(0); + + IntStream top = IntStream.range(0, Math.min(this.topPlaces, size)); + + int aroundStart = Math.max(0, playerIndex - this.aroundPlaces); + int aroundEnd = Math.min(size, playerIndex + this.aroundPlaces + 1); + IntStream around = IntStream.range(aroundStart, aroundEnd); + + IntStream bottom = IntStream.range(Math.max(0, size - this.bottomPlaces), size); + + int threshold = this.topPlaces + this.bottomPlaces + this.aroundPlaces * 2 + 1; + + IntStream indices; + if (size <= threshold) { + indices = IntStream.range(0, size); + } else if (playerIndex <= this.topPlaces + this.aroundPlaces) { + indices = IntStream.concat( + IntStream.range(0, Math.min(size, playerIndex + this.aroundPlaces + 1)), + bottom + ); + } else if (playerIndex >= size - this.bottomPlaces - this.aroundPlaces - 1) { + indices = IntStream.concat( + top, + IntStream.range(Math.max(0, playerIndex - this.aroundPlaces), size) + ); + } else { + indices = IntStream.concat(IntStream.concat(top, around), bottom); + } + + int[] display = indices.distinct().sorted().toArray(); + + for (int i = 0; i < display.length; i++) { + int idx = display[i]; + EventScoreEntry entry = scoreList.get(idx); + + String line = this.formattedLine(idx, entry.name(), entry.score()); + + objective.getScore(line).setScore(display.length - i); + } + + return scoreboard; + } + + + public void startAutomaticUpdates() { + this.scoreboardUpdateTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask( + Main.instance(), + this::updateScoreboards, + Ticks.TICKS_PER_SECOND, + Ticks.TICKS_PER_SECOND + ); + } + + public void stopAutomaticUpdates() { + if(this.scoreboardUpdateTaskId != -1) Bukkit.getScheduler().cancelTask(this.scoreboardUpdateTaskId); + this.scoreboardUpdateTaskId = -1; + } + + private void updateScore(Player p) { + this.playerScores.removeIf(entry -> entry.playerUuid().equals(p.getUniqueId())); + this.playerScores.add(new EventScoreEntry(p.getUniqueId(), p.getName(), this.scorable.getScore(p))); + } + + public void updateScoreboards() { + Bukkit.getOnlinePlayers().forEach(player -> { + this.updateScore(player); + Scoreboard scoreboard = this.buildFor(player); + player.setScoreboard(scoreboard); + }); + } + + private String formattedLine(int place, String name, int score) { + name = this.trimName(name); + return "%s. %s: %s".formatted(place+1, name, score); + } + + private String trimName(String name) { + if (name == null) return "Unknown"; + if (name.length() > 12) { + return name.substring(0, 12); + } + return name; + } +} diff --git a/settings.gradle b/settings.gradle index 9efb310..618596b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,5 @@ include 'core' include 'craftattack' include 'common' include 'varo' +include 'event'