develop-bloodmoon #10

Merged
MineTec merged 17 commits from develop-bloodmoon into master 2025-11-23 14:03:07 +00:00
9 changed files with 517 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,282 @@
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 eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonTimeListener;
import net.kyori.adventure.bossbar.BossBar;
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.GameMode;
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;
@SuppressWarnings("FieldCanBeLocal")
public class Bloodmoon extends Appliance {
public final Map<EntityType, Set<MobEffect>> affectedMobEffectMap = Map.ofEntries(
Map.entry(EntityType.ZOMBIE, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.SKELETON, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 3.5, 1)
)),
Map.entry(EntityType.SPIDER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.POISON, 4, 1),
new MobEffect.PotionMobEffect(PotionEffectType.NAUSEA, 6, 10)
)),
Map.entry(EntityType.CREEPER, Set.of(
new MobEffect.LightningMobEffect()
)),
Map.entry(EntityType.HUSK, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.STRAY, Set.of()),
Map.entry(EntityType.DROWNED, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.WITCH, Set.of()),
Map.entry(EntityType.ZOMBIE_VILLAGER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.PHANTOM, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.LEVITATION, 1.5, 3)
)),
Map.entry(EntityType.ENDERMAN, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 2.5, 2)
))
);
public final int expMultiplier = 3;
public final double mobDamageMultiplier = 2;
public final double mobHealthMultiplier = 2;
private final ThreadLocalRandom random = ThreadLocalRandom.current();
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 = 40 * Ticks.TICKS_PER_SECOND;
private final int hordeSpawnRateVariationTicks = 40 * Ticks.TICKS_PER_SECOND;
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 WeakHashMap<>();
private long lastBloodmoonStartTick = 0;
public final int ticksPerDay = 24000;
public final int bloodmoonLength = 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 bloodmoonDayInterval = 30;
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(long startTick) {
this.lastBloodmoonStartTick = startTick;
this.isActive = true;
Bukkit.getOnlinePlayers().forEach(this::addPlayerToBossBar);
this.startHordeSpawning(this.getRandomHordeSpawnDelay());
this.sendStartMessages();
}
public void stopBloodmoon() {
this.isActive = false;
Bukkit.getOnlinePlayers().forEach(player -> player.hideBossBar(this.bossBar));
this.sendStopMessages();
}
public void updateBossBar() {
long tick = Bukkit.getWorlds().getFirst().getFullTime();
long sinceStart = tick - this.lastBloodmoonStartTick;
float progress = 1f - ((float) sinceStart / this.bloodmoonLength);
if(progress < 0) progress = 1f;
this.bossBar.progress(progress);
}
public boolean isStartTick(long tick) {
long day = tick / this.ticksPerDay;
if(day % this.bloodmoonDayInterval != 0 || day - this.bloodmoonFreeDaysAtStart <= 0) return false;
long time = tick - (day * this.ticksPerDay);
return time == this.bloodmoonStartTime;
}
private int getRandomHordeSpawnDelay() {
return this.hordeSpawnRateTicks + this.random.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();
Pupsi marked this conversation as resolved
Review

hier die Variable aus dem map.get zu überschreiben sieht komisch aus
Mach doch eine neue Variable, du setzt unten ja sowiso manuell .put

hier die Variable aus dem map.get zu überschreiben sieht komisch aus Mach doch eine neue Variable, du setzt unten ja sowiso manuell .put
BukkitTask newTask = Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
if(!this.bloodmoonIsActive()) return;
this.spawnRandomHorde(player);
this.startHordeSpawning(this.getRandomHordeSpawnDelay(), player);
},
delay
);
this.hordeSpawnTasks.put(player, newTask);
}
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(Player player) {
if(!this.hordesEnabled) return;
if(!player.getGameMode().equals(GameMode.SURVIVAL)) return;
EntityType hordeEntityType = this.hordeMobList.get(this.random.nextInt(this.hordeMobList.size()));
int hordeSize = this.random.nextInt(this.hordeMinPopulation, this.hordeMaxPopulation + 1);
this.spawnHorde(player, hordeSize, hordeEntityType);
}
public void sendWarningMessage(Player p) {
p.sendMessage(Component.text("Der Blutmond waltet in diesem Augenblick!", NamedTextColor.RED));
}
public void sendAnnouncementMessages() {
this.sendMessage(Component.text("Der Blutmond naht; morgen wird er den Himmel beherrschen", NamedTextColor.GOLD));
}
public void sendPreStartMessages() {
this.sendMessage(Component.text("Der Himmel ist heute Nacht ungewöhnlich düster", NamedTextColor.DARK_PURPLE));
}
private void spawnHorde(Player player, int size, EntityType type) {
for(int i = 0; i < size; i++) {
double spawnRadiant = this.random.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 = this.random.nextInt(this.minBonusDrops, this.maxBonusDrops + 1);
List<ItemStack> result = new ArrayList<>();
for(int i = 0; i < itemCount; i++) {
result.add(this.getRandomBonusDrop());
}
return result;
}
Pupsi marked this conversation as resolved
Review

ggf. Nullable annotation und schauen, ob nulls in der drop liste irgendwelche Probleme machen

ggf. Nullable annotation und schauen, ob nulls in der drop liste irgendwelche Probleme machen
private @Nullable ItemStack getRandomBonusDrop() {
int totalWeight = this.bonusDropWeightMap.values().stream().mapToInt(value -> value).sum();
Pupsi marked this conversation as resolved
Review

eine klassenvariable random würde wahrscheinlich sinn machen

eine klassenvariable `random` würde wahrscheinlich sinn machen
int randomInt = this.random.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;
}
private void sendMessage(Component message) {
if(Bukkit.getOnlinePlayers().isEmpty()) return;
List<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers().stream()
.filter(this::getBloodmoonSetting)
.toList();
onlinePlayers.forEach(player -> player.sendMessage(message));
}
private void sendStartMessages() {
this.sendMessage(
Component.empty()
.append(Component.text("Der Blutmond ist über uns", NamedTextColor.DARK_RED, TextDecoration.BOLD))
.appendNewline()
.append(Component.text("Erfahrung vervielfacht sich und Mobs lassen reichere Beute fallen", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Mobs sind erstarkt und belegen dich mit finsteren Debuffs", NamedTextColor.RED))
);
}
private void sendStopMessages() {
this.sendMessage(Component.text("Der Blutmond weicht...", NamedTextColor.GREEN, TextDecoration.BOLD));
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new BloodmoonMonsterDeathListener(),
new BloodmoonPlayerJoinListener(),
new BloodmoonEntityDamageListener(),
new BloodmoonTimeListener()
);
}
@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.SKELETON_SKULL;
}
@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(0L);
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().mobDamageMultiplier);
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,15 @@
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());
this.getAppliance().sendWarningMessage(event.getPlayer());
}
}

View File

@@ -0,0 +1,31 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
import com.destroystokyo.paper.event.server.ServerTickStartEvent;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
public class BloodmoonTimeListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onServerTick(ServerTickStartEvent event) {
long currentTime = Bukkit.getWorlds().getFirst().getFullTime();
if(this.getAppliance().isStartTick(currentTime)) {
this.getAppliance().startBloodmoon(currentTime);
return;
}
if(this.getAppliance().isStartTick(currentTime - this.getAppliance().bloodmoonLength)) {
this.getAppliance().stopBloodmoon();
return;
}
if(currentTime % Ticks.TICKS_PER_SECOND == 0) this.getAppliance().updateBossBar();
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().ticksPerDay)) {
this.getAppliance().sendAnnouncementMessages();
return;
}
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().preStartMessageTicks)) {
this.getAppliance().sendPreStartMessages();
}
}
}