9 Commits

24 changed files with 517 additions and 313 deletions

View File

@@ -11,14 +11,15 @@ public class CraftAttackReportRepository extends ReportRepository {
public ReqResp<PlayerReports> queryReports(UUID player) { public ReqResp<PlayerReports> queryReports(UUID player) {
return this.get( return this.get(
"users/%s/reports".formatted(player.toString()), "report",
(parameters) -> parameters.addParameter("uuid", player.toString()),
PlayerReports.class PlayerReports.class
); );
} }
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) { public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post( return this.post(
"reports", "report",
data, data,
ReportUrl.class ReportUrl.class
); );

View File

@@ -23,18 +23,19 @@ public abstract class ReportRepository extends HttpRepository {
public record PlayerReports( public record PlayerReports(
List<Report> from_self, List<Report> from_self,
List<Report> to_self Object to_self
) { ) {
public record Report( public record Report(
@Nullable UUID reported, @Nullable Reporter reported,
@NotNull String reason, @NotNull String subject,
@Nullable Long created, boolean draft,
@Nullable Status status, @NotNull String status,
@NotNull String url @NotNull String url
) { ) {
public enum Status { public record Reporter(
open, @NotNull String username,
closed, @NotNull String uuid
) {
} }
} }
} }

View File

@@ -21,17 +21,18 @@ class ChatMessagesListener extends ApplianceListener<ChatMessages> {
public void onPlayerChatEvent(AsyncChatEvent event) { public void onPlayerChatEvent(AsyncChatEvent event) {
event.renderer( event.renderer(
(source, sourceDisplayName, message, viewer) -> (source, sourceDisplayName, message, viewer) ->
Component.text() Component.text("")
.append(this.getAppliance().getReportablePlayerName(source)) .append(this.getAppliance().getReportablePlayerName(source))
.append(Component.text(" > ").color(TextColor.color(Color.GRAY.asRGB()))) .append(Component.text(" > ").color(TextColor.color(Color.GRAY.asRGB())))
.append(message).color(TextColor.color(Color.SILVER.asRGB())) .append(message).color(TextColor.color(Color.SILVER.asRGB()))
.build()
); );
} }
@EventHandler(priority = EventPriority.HIGH) @EventHandler(priority = EventPriority.HIGH)
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
if(event.joinMessage() == null) return; boolean wasHidden = event.joinMessage() == null;
event.joinMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> { IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return; if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage( player.sendMessage(
@@ -44,7 +45,9 @@ class ChatMessagesListener extends ApplianceListener<ChatMessages> {
@EventHandler @EventHandler
public void onPlayerLeave(PlayerQuitEvent event) { public void onPlayerLeave(PlayerQuitEvent event) {
if(event.quitMessage() == null) return; boolean wasHidden = event.quitMessage() == null;
event.quitMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> { IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return; if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage( player.sendMessage(

View File

@@ -6,7 +6,6 @@ import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.repositories.CraftAttackReportRepository; import eu.mhsl.craftattack.spawn.common.api.repositories.CraftAttackReportRepository;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance; import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
@@ -20,9 +19,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
public class Report extends Appliance { public class Report extends Appliance {
public static Component helpText() { public static Component helpText() {
@@ -81,7 +78,7 @@ public class Report extends Appliance {
.appendNewline() .appendNewline()
.append( .append(
Component Component
.text(createdReport.data().url(), NamedTextColor.GRAY) .text(createdReport.data().url(), NamedTextColor.GRAY) // URL mit Weltkugel-Emoji
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url())) .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url()))
) )
.appendNewline() .appendNewline()
@@ -131,50 +128,43 @@ public class Report extends Appliance {
return; return;
} }
Function<List<ReportRepository.PlayerReports.Report>, List<ReportRepository.PlayerReports.Report>> filterClosed = reports -> reports.stream() List<ReportRepository.PlayerReports.Report> reports = userReports
.filter(report -> Objects.equals(report.status(), ReportRepository.PlayerReports.Report.Status.closed)) .data()
.toList(); .from_self()
.stream()
.filter(report -> !report.draft())
.toList()
.reversed();
List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed(); if(reports.isEmpty()) {
List<ReportRepository.PlayerReports.Report> reportsToSelf = filterClosed.apply(userReports.data().to_self()).reversed(); issuer.sendMessage(
Component.text()
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.text(
!reportsToSelf.isEmpty()
? "Du wurdest insgesamt %d mal von anderen Spielern gemeldet.".formatted(reportsToSelf.size())
: "Du wurdest von keinem anderen Spieler gemeldet.",
NamedTextColor.GOLD)
);
component.appendNewline();
component.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD));
reportsToOthers.forEach(report -> {
Component button = Component.text("[\uD83D\uDC41/\uD83D\uDD8A]")
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(report.url())));
Component reportedDisplayName = report.reported() != null
? Component.text(Optional.ofNullable(Bukkit.getOfflinePlayer(report.reported()).getName()).orElse(report.reported().toString()), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW);
component
.appendNewline()
.append(Component.text(" \u27A1 ", NamedTextColor.GRAY))
.append(button)
.append(Component.text(" du gegen ", NamedTextColor.GRAY))
.append(reportedDisplayName)
.append(Component.text(String.format(": %s", report.reason()), NamedTextColor.GRAY));
});
if(reportsToOthers.isEmpty()) {
component
.appendNewline()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED)) .append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline() .appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY)); .append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY))
);
return;
} }
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.newline())
.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD))
.appendNewline();
reports.forEach(report -> {
component
.append(Component.text(" - ", NamedTextColor.WHITE))
.append(
report.reported() != null
? Component.text(report.reported().username(), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW)
)
.append(Component.text(String.format(": %s", report.subject()), NamedTextColor.GRAY))
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um den Report einzusehen.", NamedTextColor.GOLD)));
component.appendNewline();
});
issuer.sendMessage(component.build()); issuer.sendMessage(component.build());
} }

View File

@@ -38,7 +38,8 @@ public class Settings extends Appliance {
BorderWarning, BorderWarning,
LocatorBar, LocatorBar,
InfoBars, InfoBars,
CoordinateDisplay CoordinateDisplay,
Bloodmoon
} }
public static Settings instance() { public static Settings instance() {

View File

@@ -1,66 +0,0 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import net.kyori.adventure.text.*;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.event.Listener;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
public class AntiFormattedBook extends Appliance {
private static final char SECTION = '\u00A7';
public boolean containsFormatting(BookMeta meta) {
if (this.hasFormattingDeep(meta.title())) return true;
if (this.hasFormattingDeep(meta.author())) return true;
for (Component c : meta.pages()) {
if (this.hasFormattingDeep(c)) return true;
}
return false;
}
private boolean hasFormattingDeep(@Nullable Component component) {
if(component == null) return false;
if (this.hastFormatting(component)) return true;
if (component instanceof TextComponent tc && tc.content().indexOf(SECTION) >= 0) return true;
if (component instanceof NBTComponent<?, ?> nbt) {
if (nbt.separator() != null && this.hasFormattingDeep(nbt.separator())) return true;
}
for (Component child : component.children()) {
if (this.hasFormattingDeep(child)) return true;
}
return false;
}
private boolean hastFormatting(Component component) {
Style style = component.style();
TextColor color = style.color();
if (color != null) return true;
if (style.font() != null) return true;
if (style.insertion() != null && !Objects.requireNonNull(style.insertion()).isEmpty()) return true;
for (var decoration : style.decorations().entrySet()) {
if (decoration.getValue() == TextDecoration.State.TRUE) return true;
}
return style.hoverEvent() != null || style.clickEvent() != null;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new BookEditListener()
);
}
}

View File

@@ -1,36 +0,0 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook;
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerEditBookEvent;
import org.bukkit.inventory.meta.BookMeta;
import java.util.List;
class BookEditListener extends ApplianceListener<AntiFormattedBook> {
@EventHandler
public void onBookEdit(PlayerEditBookEvent event) {
Player player = event.getPlayer();
BookMeta meta = event.getNewBookMeta();
if (this.getAppliance().containsFormatting(meta)) {
Main.instance().getAppliance(AcInform.class).notifyAdmins(
"internal",
player.getName(),
"illegalBookFormatting",
1f
);
BookMeta sanitized = meta.clone();
sanitized.title(null);
sanitized.author(null);
//noinspection ResultOfMethodCallIgnored
sanitized.pages(List.of(Component.empty()));
event.setNewBookMeta(sanitized);
}
}
}

View File

@@ -1,78 +0,0 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker;
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BundleMeta;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
public class AntiIllegalBundlePicker extends Appliance {
private static final int visibleSlotsInBundle = 9;
public void trackBundle(InventoryClickEvent event) {
ItemStack bundle = Objects.requireNonNull(event.getCurrentItem());
final int rawSlot = event.getRawSlot();
final Player player = (Player) event.getWhoClicked();
final InventoryView view = event.getView();
final List<ItemStack> before = this.getBundleContents(bundle);
Bukkit.getScheduler().runTask(Main.instance(), () -> {
ItemStack afterStack = view.getItem(rawSlot);
if(afterStack == null || afterStack.getType() != Material.BUNDLE) return;
List<ItemStack> after = this.getBundleContents(afterStack);
int removedSlotIndex = this.findRemoved(before, after);
if(removedSlotIndex >= visibleSlotsInBundle) {
Main.instance().getAppliance(AcInform.class).notifyAdmins(
"internal",
player.getName(),
"illegalBundlePick",
(float) removedSlotIndex
);
}
});
}
private int findRemoved(@NotNull List<ItemStack> before, @NotNull List<ItemStack> after) {
for (int i = 0; i < Math.max(before.size(), after.size()); i++) {
ItemStack a = i < after.size() ? after.get(i) : null;
ItemStack b = i < before.size() ? before.get(i) : null;
if (b == null && a == null) continue;
if (b == null) throw new IllegalStateException("Size of bundle was smaller before pickup Action");
if (a == null) return i;
if (!a.isSimilar(b)) return i;
if (a.getAmount() != b.getAmount()) return i;
}
throw new IllegalStateException("Failed to find picked Item in bundle");
}
private List<ItemStack> getBundleContents(@NotNull ItemStack bundle) {
if (bundle.getType() != Material.BUNDLE)
throw new IllegalStateException("ItemStack is not a bundle");
BundleMeta meta = (BundleMeta) bundle.getItemMeta();
return meta.getItems().stream()
.map(ItemStack::clone)
.collect(Collectors.toCollection(ArrayList::new));
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new OnBundlePickListener()
);
}
}

View File

@@ -1,18 +0,0 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
class OnBundlePickListener extends ApplianceListener<AntiIllegalBundlePicker> {
@EventHandler
public void onBundlePick(InventoryClickEvent event) {
if(!event.getAction().equals(InventoryAction.PICKUP_FROM_BUNDLE)) return;
final ItemStack bundle = event.getCurrentItem();
if (bundle == null || bundle.getType() != Material.BUNDLE) return;
this.getAppliance().trackBundle(event);
}
}

View File

@@ -5,7 +5,6 @@ import eu.mhsl.craftattack.spawn.core.util.statistics.ServerMonitor;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -20,12 +19,7 @@ import java.util.stream.Stream;
public class ComponentUtil { public class ComponentUtil {
public static TextComponent pleaseWait() { public static TextComponent pleaseWait() {
return Component.text("\uD83D\uDCBE Daten werden geladen... Warte einen Augenblick!", NamedTextColor.GRAY); return Component.text("Bitte warte einen Augenblick...", NamedTextColor.GRAY);
}
public static TextComponent clickLink(String url) {
return Component.text("Klicke, um zu öffnen: \uD83D\uDD17[%s]".formatted(url))
.clickEvent(ClickEvent.openUrl(url));
} }
public static Component clearedSpace() { public static Component clearedSpace() {

View File

@@ -1,10 +1,13 @@
package eu.mhsl.craftattack.spawn.craftattack.api.repositories; package eu.mhsl.craftattack.spawn.craftattack.api.repositories;
import com.google.common.reflect.TypeToken;
import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository; import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi; import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class FeedbackRepository extends HttpRepository { public class FeedbackRepository extends HttpRepository {
@@ -12,15 +15,14 @@ public class FeedbackRepository extends HttpRepository {
super(CraftAttackApi.getBaseUri(), new RequestModifier(null, CraftAttackApi::withAuthorizationHeader)); super(CraftAttackApi.getBaseUri(), new RequestModifier(null, CraftAttackApi::withAuthorizationHeader));
} }
public record Request(String event, String title, List<UUID> users) { public record Request(String event, List<UUID> users) {
} }
public record Response(List<Feedback> feedback) { public ReqResp<Map<UUID, String>> createFeedbackUrls(Request data) {
public record Feedback(UUID uuid, String url) { final Type responseType = new TypeToken<Map<UUID, String>>() {
} }.getType();
} ReqResp<Object> rawData = this.post("feedback", data, Object.class);
// TODO: use convertToTypeToken from ReqResp
public ReqResp<Response> createFeedbackUrls(Request data) { return new ReqResp<>(rawData.status(), this.gson.fromJson(this.gson.toJson(rawData.data()), responseType));
return this.post("feedback", data, Response.class);
} }
} }

View File

@@ -4,7 +4,6 @@ import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi; import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class WhitelistRepository extends HttpRepository { public class WhitelistRepository extends HttpRepository {
@@ -17,15 +16,17 @@ public class WhitelistRepository extends HttpRepository {
String username, String username,
String firstname, String firstname,
String lastname, String lastname,
List<Strike> strikes Long banned_until,
Long outlawed_until
) { ) {
public record Strike(int at, int weight) {
}
} }
private record UserQuery(UUID uuid) {}
public ReqResp<UserData> getUserData(UUID userId) { public ReqResp<UserData> getUserData(UUID userId) {
return this.get( return this.post(
"users/%s".formatted(userId.toString()), "player",
new UserQuery(userId),
UserData.class UserData.class
); );
} }

View File

@@ -0,0 +1,224 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
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.craftattack.appliances.gameplay.bloodmoon.commands.BloodmoonCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonEntityDamageListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonMonsterDeathListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonPlayerJoinListener;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class Bloodmoon extends Appliance {
// für alle Dimensionen? einstellbar machen?
public final Map<EntityType, Set<MobEffect>> affectedMobEffectMap = Map.of(
EntityType.ZOMBIE, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.SKELETON, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 3.5, 1)
),
EntityType.SPIDER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.POISON, 4, 1),
new MobEffect.PotionMobEffect(PotionEffectType.NAUSEA, 6, 10)
),
EntityType.CREEPER, Set.of(
new MobEffect.LightningMobEffect()
),
EntityType.HUSK, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.DROWNED, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.WITCH, Set.of(),
EntityType.ZOMBIE_VILLAGER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.PHANTOM, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.LEVITATION, 1.5, 3)
),
EntityType.ENDERMAN, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 2.5, 2)
)
);
public final int expMultiplier = 4;
public final double mobDamageMultipier = 2;
public final double mobHealthMultiplier = 3;
private boolean isActive = false;
private final BossBar bossBar = BossBar.bossBar(
Component.text("Blutmond", NamedTextColor.DARK_RED),
1f,
BossBar.Color.RED,
BossBar.Overlay.NOTCHED_12,
Set.of(BossBar.Flag.CREATE_WORLD_FOG, BossBar.Flag.DARKEN_SCREEN)
);
private final boolean hordesEnabled = true;
private final int hordeSpawnRateTicks = 800;
private final int hordeSpawnRateVariationTicks = 800;
private final int hordeMinPopulation = 3;
private final int hordeMaxPopulation = 10;
private final int hordeSpawnDistance = 10;
private final List<EntityType> hordeMobList = List.of(
EntityType.ZOMBIE,
EntityType.SKELETON,
EntityType.SPIDER
);
private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new HashMap<>();
private final int bloodmoonLegth = 12000;
private final int bloodmoonStartTime = 12000;
private final int daysBetweenBloodmoons = 1;
private final int minBonusDrops = 1;
private final int maxBonusDrops = 4;
private final Map<ItemStack, Integer> bonusDropWeightMap = Map.of(
new ItemStack(Material.IRON_INGOT, 5), 10,
new ItemStack(Material.GOLD_INGOT, 2), 5,
new ItemStack(Material.DIAMOND, 1), 1,
new ItemStack(Material.IRON_BLOCK, 1), 5,
new ItemStack(Material.GOLD_BLOCK, 1), 2
);
@Override
public void onEnable() {
Settings.instance().declareSetting(BloodmoonSetting.class);
}
public boolean bloodmoonIsActive() {
return this.isActive;
}
public void startBloodmoon() {
this.isActive = true;
Bukkit.getOnlinePlayers().forEach(this::addPlayerToBossBar);
this.startHordeSpawning(this.getRandomHordeSpawnDelay());
}
private int getRandomHordeSpawnDelay() {
return this.hordeSpawnRateTicks + ThreadLocalRandom.current().nextInt(this.hordeSpawnRateVariationTicks);
}
private void startHordeSpawning(int delay) {
Bukkit.getOnlinePlayers().forEach(player -> this.startHordeSpawning(delay, player));
}
private void startHordeSpawning(int delay, Player player) {
@Nullable BukkitTask task = this.hordeSpawnTasks.get(player);
if(task != null) task.cancel();
task = Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
if(!this.bloodmoonIsActive()) return;
this.spawnRandomHorde();
this.startHordeSpawning(this.getRandomHordeSpawnDelay());
},
delay
);
this.hordeSpawnTasks.put(player, task);
}
public void stopBloodmoon() {
this.isActive = false;
Bukkit.getOnlinePlayers().forEach(player -> player.hideBossBar(this.bossBar));
}
public void addPlayerToBossBar(Player player) {
if(!this.getBloodmoonSetting(player)) return;
player.showBossBar(this.bossBar);
}
public boolean isAffectedMob(Entity entity) {
return this.affectedMobEffectMap.containsKey(entity.getType());
}
public boolean getBloodmoonSetting(Player player) {
return Settings.instance().getSetting(
player,
Settings.Key.Bloodmoon,
Boolean.class
);
}
public void spawnRandomHorde() {
if(Bukkit.getOnlinePlayers().isEmpty()) return;
if(!this.hordesEnabled) return;
List<? extends Player> onlinePlayerList = Bukkit.getOnlinePlayers().stream()
.filter(this::getBloodmoonSetting)
// .filter(player -> player.getGameMode().equals(GameMode.SURVIVAL))
.toList();
ThreadLocalRandom random = ThreadLocalRandom.current();
Player player = onlinePlayerList.get(random.nextInt(onlinePlayerList.size()));
EntityType hordeEntityType = this.hordeMobList.get(random.nextInt(this.hordeMobList.size()));
int hordeSize = random.nextInt(this.hordeMinPopulation, this.hordeMaxPopulation + 1);
this.spawnHorde(player, hordeSize, hordeEntityType);
}
private void spawnHorde(Player player, int size, EntityType type) {
for(int i = 0; i < size; i++) {
double spawnRadiant = ThreadLocalRandom.current().nextDouble(0, 2*Math.PI);
Location mobSpawnLocation = player.getLocation().add(
Math.sin(spawnRadiant)*this.hordeSpawnDistance,
0,
Math.cos(spawnRadiant)*this.hordeSpawnDistance
);
mobSpawnLocation.setY(player.getWorld().getHighestBlockYAt(mobSpawnLocation) + 1);
player.getWorld().spawnEntity(mobSpawnLocation, type);
player.getWorld().strikeLightningEffect(mobSpawnLocation);
}
}
public List<ItemStack> getRandomBonusDrops() {
int itemCount = ThreadLocalRandom.current().nextInt(this.minBonusDrops, this.maxBonusDrops + 1);
List<ItemStack> result = new ArrayList<>();
for(int i = 0; i < itemCount; i++) {
result.add(this.getRandomBonusDrop());
}
return result;
}
private ItemStack getRandomBonusDrop() {
int totalWeight = this.bonusDropWeightMap.values().stream().mapToInt(value -> value).sum();
int randomInt = ThreadLocalRandom.current().nextInt(0, totalWeight + 1);
int cumulativeWeight = 0;
for(Map.Entry<ItemStack, Integer> entry : this.bonusDropWeightMap.entrySet()) {
cumulativeWeight += entry.getValue();
if(randomInt <= cumulativeWeight) return entry.getKey();
}
return null;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new BloodmoonMonsterDeathListener(),
new BloodmoonPlayerJoinListener(),
new BloodmoonEntityDamageListener()
);
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new BloodmoonCommand()
);
}
}

View File

@@ -0,0 +1,56 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
public class BloodmoonSetting extends BoolSetting implements CategorizedSetting {
public BloodmoonSetting() {
super(Settings.Key.Bloodmoon);
}
@Override
public SettingCategory category() {
return SettingCategory.Misc; // TODO: mehr als 8 bug fixen
}
@Override
protected String title() {
return "Blutmond";
}
@Override
protected String description() {
return "Kämpfe während dem Blutmond gegen stärkere Monster mit besseren Drops. Kann nicht während dem Blutmond geändert werden!";
}
@Override
protected Material icon() {
return Material.CLOCK;
}
@Override
protected Boolean defaultValue() {
return true;
}
// TODO: Settings deaktivierbar machen
@Override
protected void change(Player player, ClickType clickType) {
if(Main.instance().getAppliance(Bloodmoon.class).bloodmoonIsActive()) {
InteractSounds.of(player).delete();
player.sendMessage(Component.text("Während dem Blutmond kann diese Einstellung nicht geändert werden!", NamedTextColor.RED));
return;
}
super.change(player, clickType);
}
}

View File

@@ -0,0 +1,29 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import net.kyori.adventure.util.Ticks;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public abstract class MobEffect {
public abstract void apply(Player p);
public static class PotionMobEffect extends MobEffect {
private final PotionEffect effect;
public PotionMobEffect(PotionEffectType type, double seconds, int amplifier) {
this.effect = new PotionEffect(type, (int) (seconds * Ticks.TICKS_PER_SECOND), amplifier);
}
@Override
public void apply(Player p) {
p.addPotionEffect(this.effect);
}
}
public static class LightningMobEffect extends MobEffect {
@Override
public void apply(Player p) {
p.getWorld().strikeLightning(p.getLocation());
}
}
}

View File

@@ -0,0 +1,44 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.commands;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
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 BloodmoonCommand extends ApplianceCommand<Bloodmoon> {
public BloodmoonCommand() {
super("bloodmoon");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length == 0) {
sender.sendMessage("Bloodmoon is currently %s.".formatted(this.getAppliance().bloodmoonIsActive() ? "active" : "inactive"));
return;
}
switch(args[0]) {
case "start": {
this.getAppliance().startBloodmoon();
sender.sendMessage("Started bloodmoon.");
break;
}
case "stop": {
this.getAppliance().stopBloodmoon();
sender.sendMessage("Stopped bloodmoon.");
break;
}
default: throw new Error("No such option: '%s' !".formatted(args[0]));
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 1) return List.of("start", "stop");
return null;
}
}

View File

@@ -0,0 +1,35 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class BloodmoonEntityDamageListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
Entity damagerEntity = event.getDamager();
if(damagerEntity instanceof Projectile projectile) {
if(!(projectile.getShooter() instanceof Entity shooter)) return;
damagerEntity = shooter;
}
if(!(damagerEntity instanceof LivingEntity damager && event.getEntity() instanceof LivingEntity receiver)) return;
if(event.getFinalDamage() == 0) return;
if(this.getAppliance().isAffectedMob(damager) && receiver instanceof Player player) {
if(!this.getAppliance().getBloodmoonSetting(player)) return;
event.setDamage(event.getDamage() * this.getAppliance().mobDamageMultipier);
this.getAppliance().affectedMobEffectMap.get(damager.getType()).forEach(mobEffect -> mobEffect.apply(player));
return;
}
if(this.getAppliance().isAffectedMob(receiver) && damager instanceof Player player) {
if(!this.getAppliance().getBloodmoonSetting(player)) return;
event.setDamage(event.getDamage() / this.getAppliance().mobHealthMultiplier);
}
}
}

View File

@@ -0,0 +1,23 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDeathEvent;
public class BloodmoonMonsterDeathListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onMonsterDeath(EntityDeathEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
if(!(event.getDamageSource().getCausingEntity() instanceof Player player)) return;
if(!this.getAppliance().getBloodmoonSetting(player)) return;
LivingEntity entity = event.getEntity();
if(!this.getAppliance().isAffectedMob(entity)) return;
event.setDroppedExp(event.getDroppedExp() * this.getAppliance().expMultiplier);
event.getDrops().addAll(this.getAppliance().getRandomBonusDrops());
entity.getWorld().strikeLightningEffect(entity.getLocation());
}
}

View File

@@ -0,0 +1,14 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class BloodmoonPlayerJoinListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
this.getAppliance().addPlayerToBossBar(event.getPlayer());
}
}

View File

@@ -1,7 +1,7 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback; package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.spawn.craftattack.api.repositories.FeedbackRepository; import eu.mhsl.craftattack.spawn.craftattack.api.repositories.FeedbackRepository;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance; import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
@@ -9,6 +9,7 @@ import eu.mhsl.craftattack.spawn.core.api.HttpStatus;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@@ -17,27 +18,32 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
public class Feedback extends Appliance { public class Feedback extends Appliance {
public Feedback() { public Feedback() {
super("feedback"); super("feedback");
} }
public void requestFeedback(String eventName, String title, List<Player> receivers, @Nullable String question) { public void requestFeedback(String eventName, List<Player> receivers, @Nullable String question) {
ReqResp<FeedbackRepository.Response> response = this.queryRepository(FeedbackRepository.class).createFeedbackUrls( ReqResp<Map<UUID, String>> response = this.queryRepository(FeedbackRepository.class).createFeedbackUrls(
new FeedbackRepository.Request(eventName, title, receivers.stream().map(Entity::getUniqueId).toList()) new FeedbackRepository.Request(eventName, receivers.stream().map(Entity::getUniqueId).toList())
); );
if(response.status() != HttpStatus.OK) throw new RuntimeException(); System.out.println(response.toString());
System.out.println(response.status());
if(response.status() != HttpStatus.CREATED) throw new RuntimeException();
Component border = Component.text("-".repeat(40), NamedTextColor.GRAY); Component border = Component.text("-".repeat(40), NamedTextColor.GRAY);
receivers.forEach(player -> { receivers.forEach(player -> {
String feedbackUrl = response.data().feedback().stream() String feedbackUrl = response.data().get(player.getUniqueId());
.filter(feedback -> feedback.uuid().equals(player.getUniqueId())) if(feedbackUrl == null) {
.findFirst() Main.logger().warning(String.format("FeedbackUrl not found for player '%s' from backend!", player.getUniqueId()));
.orElseThrow() return;
.url(); }
ComponentBuilder<TextComponent, TextComponent.Builder> message = Component.text() ComponentBuilder<TextComponent, TextComponent.Builder> message = Component.text()
.append(border) .append(border)
@@ -52,7 +58,8 @@ public class Feedback extends Appliance {
message message
.append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN) .append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN)
.hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(feedbackUrl)))) .clickEvent(ClickEvent.openUrl(feedbackUrl)))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um Feedback zu geben.")))
.appendNewline() .appendNewline()
.append(border); .append(border);

View File

@@ -22,7 +22,6 @@ class FeedbackCommand extends ApplianceCommand.PlayerChecked<Feedback> {
Main.instance(), Main.instance(),
() -> this.getAppliance().requestFeedback( () -> this.getAppliance().requestFeedback(
"self-issued-ingame", "self-issued-ingame",
"Dein Feedback an uns",
List.of(this.getPlayer()), List.of(this.getPlayer()),
null null
) )

View File

@@ -20,7 +20,6 @@ class RequestFeedbackCommand extends ApplianceCommand<Feedback> {
Main.instance(), Main.instance(),
() -> this.getAppliance().requestFeedback( () -> this.getAppliance().requestFeedback(
"admin-issued-ingame", "admin-issued-ingame",
"Hilf uns dein Spielerlebnis zu verbessern!",
new ArrayList<>(Bukkit.getOnlinePlayers()), String.join(" ", args) new ArrayList<>(Bukkit.getOnlinePlayers()), String.join(" ", args)
) )
); );

View File

@@ -1,21 +0,0 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.strike;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import java.time.Duration;
import java.util.Map;
public class Strike extends Appliance {
public Strike() {
super("strike");
}
private final Map<Integer, Duration> strikePunishmentMap = Map.of(
1, Duration.ofHours(1),
2, Duration.ofHours(24),
3, Duration.ofDays(3),
4, Duration.ofDays(7)
);
}

View File

@@ -47,7 +47,7 @@ public class Whitelist extends Appliance {
player.getUniqueId() player.getUniqueId()
); );
} }
this.queryAppliance(Outlawed.class).updateForcedStatus(player, this.timestampRelevant(0L)); // TODO this.queryAppliance(Outlawed.class).updateForcedStatus(player, this.timestampRelevant(user.outlawed_until()));
String purePlayerName = Floodgate.isBedrock(player) String purePlayerName = Floodgate.isBedrock(player)
? Floodgate.getBedrockPlayer(player).getUsername() ? Floodgate.getBedrockPlayer(player).getUsername()
@@ -67,14 +67,14 @@ public class Whitelist extends Appliance {
Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name)); Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name));
boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false); boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false);
WhitelistRepository.UserData user = overrideCheck WhitelistRepository.UserData user = overrideCheck
? new WhitelistRepository.UserData(uuid, name, "", "", List.of()) ? new WhitelistRepository.UserData(uuid, name, "", "", 0L, 0L)
: this.fetchUserData(uuid); : this.fetchUserData(uuid);
this.userData.put(uuid, user); this.userData.put(uuid, user);
Main.logger().info(String.format("got userdata %s", user.toString())); Main.logger().info(String.format("got userdata %s", user.toString()));
if(this.timestampRelevant(0L)) { //TODO if(this.timestampRelevant(user.banned_until())) {
Instant bannedDate = new Date(0 * 1000L) // TODO Instant bannedDate = new Date(user.banned_until() * 1000L)
.toInstant() .toInstant()
.plus(1, ChronoUnit.HOURS); .plus(1, ChronoUnit.HOURS);