Skip to content

Commit 28ce803

Browse files
committed
feat: Modify (first) EnderDragon fight
1 parent 9aa4392 commit 28ce803

File tree

4 files changed

+318
-1
lines changed

4 files changed

+318
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package de.kiridevs.ksmpplugin;
2+
3+
import net.kyori.adventure.text.Component;
4+
import net.kyori.adventure.text.format.NamedTextColor;
5+
import org.bukkit.Location;
6+
import org.bukkit.NamespacedKey;
7+
import org.bukkit.World;
8+
import org.bukkit.block.Biome;
9+
import org.bukkit.boss.DragonBattle;
10+
import org.bukkit.entity.Entity;
11+
import org.bukkit.event.entity.EntityDamageEvent;
12+
import org.bukkit.persistence.PersistentDataContainer;
13+
import org.bukkit.persistence.PersistentDataType;
14+
15+
import java.util.Map;
16+
17+
public class Util {
18+
public static void applyDamageModifier(EntityDamageEvent event, double modifier) {
19+
double rawDamage = event.getDamage();
20+
double effectiveDamage = rawDamage * modifier;
21+
22+
if (effectiveDamage == 0) event.setCancelled(true);
23+
else event.setDamage(effectiveDamage);
24+
}
25+
26+
public static boolean entityIsPartOfFirstDragonBattle(Entity entity) {
27+
Location loc = entity.getLocation();
28+
World world = loc.getWorld();
29+
// The dragon battle happens on the end island
30+
if (!world.getBiome(loc).equals(Biome.THE_END)) return false;
31+
32+
DragonBattle battle = world.getEnderDragonBattle();
33+
if (battle == null) return false; // Can't be part of a non-existing dragon fight
34+
return !battle.hasBeenPreviouslyKilled();
35+
}
36+
37+
public static class HealthIndicators {
38+
public static final Map<Integer, NamedTextColor> HEALTH_COLOR_MAP = Map.ofEntries(
39+
Map.entry(3, NamedTextColor.GREEN),
40+
Map.entry(2, NamedTextColor.YELLOW),
41+
Map.entry(1, NamedTextColor.RED)
42+
);
43+
public static final Component HEALTH_SEPERATOR_COMPONENT = Component.text(" / ").color(NamedTextColor.BLUE);
44+
45+
public static void update(Entity forEntity, NamespacedKey pdcKey, int maxHealth) {
46+
Component maxHealthComponent = Component.text(maxHealth).color(NamedTextColor.BLUE);
47+
Component suffixComponent = HEALTH_SEPERATOR_COMPONENT.append(maxHealthComponent);
48+
49+
PersistentDataContainer pdc = forEntity.getPersistentDataContainer();
50+
int currentHealth = pdc.getOrDefault(pdcKey, PersistentDataType.INTEGER, maxHealth);
51+
NamedTextColor currentHealthColor = HEALTH_COLOR_MAP.getOrDefault(currentHealth, NamedTextColor.LIGHT_PURPLE);
52+
Component currentHealthComponent = Component.text(currentHealth).color(currentHealthColor);
53+
54+
Component newHealthIndicator = currentHealthComponent.append(suffixComponent);
55+
forEntity.customName(newHealthIndicator);
56+
forEntity.setCustomNameVisible(true);
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package de.kiridevs.ksmpplugin.features;
2+
3+
import de.kiridevs.ksmpplugin.Util;
4+
import de.kiridevs.ksmpplugin.main.KiriSmpPlugin;
5+
import io.papermc.paper.event.block.DragonEggFormEvent;
6+
import net.kyori.adventure.text.Component;
7+
import net.kyori.adventure.text.format.NamedTextColor;
8+
import org.bukkit.Bukkit;
9+
import org.bukkit.World;
10+
import org.bukkit.WorldBorder;
11+
import org.bukkit.boss.DragonBattle;
12+
import org.bukkit.entity.AreaEffectCloud;
13+
import org.bukkit.entity.EnderDragon;
14+
import org.bukkit.entity.Entity;
15+
import org.bukkit.entity.Player;
16+
import org.bukkit.event.EventHandler;
17+
import org.bukkit.event.Listener;
18+
import org.bukkit.event.entity.EntityDamageByEntityEvent;
19+
import org.bukkit.event.entity.EntityDamageEvent;
20+
import org.bukkit.event.player.PlayerChangedWorldEvent;
21+
import org.bukkit.projectiles.ProjectileSource;
22+
23+
import java.util.Map;
24+
25+
// This class groups all functionality responsible for making the EnderDragon stronger for the first dragon fight
26+
public class DragonBuff implements Listener {
27+
// Damage is reduced across the board to adjust for player amount
28+
// This modifier is applied after cause-specific modifiers
29+
public static final Float INCOMING_MODIFIER = 0.5f;
30+
31+
// This modifier is applied to all damage coming *from* the dragon,
32+
// or AreaEffectCloud|s of its breath
33+
public static final Float OUTGOING_MODIFIER = 2f;
34+
public static final int END_WORLDBORDER_SIZE = 59_999_968;
35+
36+
static final Map<EntityDamageEvent.DamageCause, Float> incomingDamageModifiers = Map.ofEntries(
37+
// Explosions don't do any damage
38+
Map.entry(EntityDamageEvent.DamageCause.ENTITY_EXPLOSION, 0f),
39+
Map.entry(EntityDamageEvent.DamageCause.BLOCK_EXPLOSION, 0f),
40+
41+
// Melee Attacks only deal 35% of the damage
42+
Map.entry(EntityDamageEvent.DamageCause.ENTITY_ATTACK, 0.35f),
43+
Map.entry(EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK, 0.35f)
44+
);
45+
46+
final KiriSmpPlugin plugin;
47+
48+
public DragonBuff(KiriSmpPlugin plugin) {
49+
this.plugin = plugin;
50+
}
51+
52+
// This limits the world border during the fight
53+
@EventHandler
54+
public void onDimensionSwitch(PlayerChangedWorldEvent event) {
55+
Player player = event.getPlayer();
56+
World world = player.getWorld();
57+
if (world.getPlayerCount() > 1) return; // There are other players already, don't touch anything
58+
59+
if (!Util.entityIsPartOfFirstDragonBattle(player)) return;
60+
DragonBattle battle = world.getEnderDragonBattle();
61+
assert battle != null;
62+
63+
// Rename the first dragon due to its higher strength
64+
// Dragon might take a little while to spawn, so let's wait for that
65+
Bukkit.getScheduler().scheduleSyncDelayedTask(this.plugin, () -> {
66+
EnderDragon dragon = battle.getEnderDragon();
67+
if (dragon == null) return; // The dragon somehow already died
68+
this.plugin.log.info("Changed dragon name");
69+
dragon.customName(Component.text("Elder Dragon").color(NamedTextColor.AQUA));
70+
}, 5); // 5t * 20t/s = 0.25s
71+
72+
// Limit world border for the first dragon fight
73+
WorldBorder border = player.getWorld().getWorldBorder();
74+
border.setCenter(0, 0);
75+
border.setSize(500);
76+
border.setWarningDistance(50);
77+
border.setDamageAmount(2);
78+
}
79+
80+
// This EventHandler is responsible for modifying any incoming damage (AGAINST the dragon)
81+
@EventHandler
82+
public void onEntityDamage(EntityDamageEvent event) {
83+
Entity target = event.getEntity();
84+
if (!(target instanceof EnderDragon)) return;
85+
if (!Util.entityIsPartOfFirstDragonBattle(target)) return;
86+
87+
// Apply whatever causeModifier applies
88+
EntityDamageEvent.DamageCause dmgCause = event.getCause();
89+
float effectiveModifer = INCOMING_MODIFIER;
90+
for (EntityDamageEvent.DamageCause testCause : incomingDamageModifiers.keySet()) {
91+
if (!testCause.equals(dmgCause)) continue;
92+
93+
Float causeModifier = incomingDamageModifiers.get(testCause);
94+
effectiveModifer = effectiveModifer * causeModifier;
95+
}
96+
97+
Util.applyDamageModifier(event, effectiveModifer);
98+
}
99+
100+
// This EventHandler is responsible for modifying any outgoing damage (FROM the dragon)
101+
@EventHandler
102+
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
103+
Entity damager = event.getDamager();
104+
if (damager instanceof EnderDragon) {
105+
Util.applyDamageModifier(event, OUTGOING_MODIFIER);
106+
} else if (damager instanceof AreaEffectCloud) {
107+
AreaEffectCloud cloud = (AreaEffectCloud) event.getDamager();
108+
ProjectileSource source = cloud.getSource();
109+
if (source instanceof EnderDragon) Util.applyDamageModifier(event, OUTGOING_MODIFIER);
110+
}
111+
}
112+
113+
// This resets the world border when the first dragon fight concludes
114+
@EventHandler
115+
public void onFirstDragonDeath(DragonEggFormEvent event) {
116+
WorldBorder border = event.getBlock().getWorld().getWorldBorder();
117+
border.setCenter(0, 0);
118+
border.setSize(END_WORLDBORDER_SIZE);
119+
border.setWarningDistance(5);
120+
}
121+
122+
public void init() {
123+
this.plugin.log.info("features: DragonBuff: Initializing");
124+
Bukkit.getPluginManager().registerEvents(this, this.plugin);
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package de.kiridevs.ksmpplugin.features;
2+
3+
import de.kiridevs.ksmpplugin.Util;
4+
import de.kiridevs.ksmpplugin.main.KiriSmpPlugin;
5+
import org.bukkit.Bukkit;
6+
import org.bukkit.Location;
7+
import org.bukkit.NamespacedKey;
8+
import org.bukkit.World;
9+
import org.bukkit.entity.EnderCrystal;
10+
import org.bukkit.entity.Entity;
11+
import org.bukkit.entity.Player;
12+
import org.bukkit.event.EventHandler;
13+
import org.bukkit.event.Listener;
14+
import org.bukkit.event.entity.EntityDamageByBlockEvent;
15+
import org.bukkit.event.entity.EntityDamageByEntityEvent;
16+
import org.bukkit.event.entity.EntityDamageEvent;
17+
import org.bukkit.event.entity.EntityPlaceEvent;
18+
import org.bukkit.event.player.PlayerChangedWorldEvent;
19+
import org.bukkit.persistence.PersistentDataContainer;
20+
import org.bukkit.persistence.PersistentDataType;
21+
import org.bukkit.util.Vector;
22+
23+
import java.util.Collection;
24+
25+
// This class groups all functionality responsible for making EndCrystals stronger for the first dragon fight
26+
public class EndCrystalBuff implements Listener {
27+
public static final int END_CRYSTAL_MAX_HEALTH = 3;
28+
29+
KiriSmpPlugin plugin;
30+
NamespacedKey crystalHealthNsKey;
31+
32+
public EndCrystalBuff(KiriSmpPlugin plugin) {
33+
this.plugin = plugin;
34+
this.crystalHealthNsKey = new NamespacedKey(this.plugin, "endcrystal_health");
35+
}
36+
37+
@EventHandler
38+
public void onDimensionSwitch(PlayerChangedWorldEvent event) {
39+
Player player = event.getPlayer();
40+
if (player.getWorld().getPlayerCount() > 1) return; // There are other players already, don't touch anything
41+
if (!Util.entityIsPartOfFirstDragonBattle(player)) return;
42+
43+
// Update (Initialize) health indicators for all end crystals
44+
World endWorld = player.getWorld();
45+
Collection<EnderCrystal> crystals = endWorld.getEntitiesByClass(EnderCrystal.class);
46+
for (EnderCrystal crystal : crystals) {
47+
Util.HealthIndicators.update(crystal, this.crystalHealthNsKey, END_CRYSTAL_MAX_HEALTH);
48+
}
49+
}
50+
51+
// This handler creates health indicators for crystals placed during the fight
52+
@EventHandler
53+
public void onEntityPlace(EntityPlaceEvent event) {
54+
if (!(event.getEntity() instanceof EnderCrystal crystal)) return;
55+
if (!Util.entityIsPartOfFirstDragonBattle(crystal)) return;
56+
Util.HealthIndicators.update(crystal, this.crystalHealthNsKey, END_CRYSTAL_MAX_HEALTH);
57+
}
58+
59+
// Disallow any damage coming from blocks
60+
@EventHandler
61+
public void onCrystalDamageByBlock(EntityDamageByBlockEvent event) {
62+
Entity damagee = event.getEntity();
63+
if (!(damagee instanceof EnderCrystal)) return; // No EnderCrystal => No action
64+
if (Util.entityIsPartOfFirstDragonBattle(damagee)) event.setCancelled(true);
65+
}
66+
67+
@EventHandler
68+
public void onEntityDamageByEntityEvent(EntityDamageByEntityEvent event) {
69+
// Only act on EndCrystals in the first dragon fight
70+
Entity damagee = event.getEntity();
71+
if (!(damagee instanceof EnderCrystal crystal)) return;
72+
if (!Util.entityIsPartOfFirstDragonBattle(crystal)) return;
73+
74+
// Disallow any non-player damage
75+
Entity damager = event.getDamager();
76+
if (!(damager instanceof Player player)) {
77+
event.setCancelled(true);
78+
return;
79+
}
80+
81+
// Disallow any non-melee damage
82+
EntityDamageEvent.DamageCause cause = event.getCause();
83+
if (!cause.equals(EntityDamageEvent.DamageCause.ENTITY_ATTACK)) {
84+
event.setCancelled(true);
85+
return;
86+
}
87+
88+
// Calculate custom crystal knockback
89+
Location crystalLocation = crystal.getLocation();
90+
Location playerLocation = player.getLocation();
91+
92+
Vector delta = (playerLocation.subtract(crystalLocation)).toVector();
93+
double lengthSquared = delta.lengthSquared();
94+
Vector knockback = delta.multiply(1 / lengthSquared).multiply(10);
95+
96+
// Create a small explosion, then apply customKnockback, schedule big boom for later
97+
World crystalWorld = crystal.getWorld();
98+
crystalWorld.createExplosion(crystalLocation, 7);
99+
player.setVelocity(knockback);
100+
Bukkit.getScheduler().scheduleSyncDelayedTask(this.plugin, () -> {
101+
crystalWorld.createExplosion(crystalLocation, 20);
102+
}, 4); // 4t * 20t/s = 0.25s
103+
104+
// Reduce crystal health
105+
PersistentDataContainer pdc = crystal.getPersistentDataContainer();
106+
int previousHealth = pdc.getOrDefault(
107+
this.crystalHealthNsKey,
108+
PersistentDataType.INTEGER,
109+
END_CRYSTAL_MAX_HEALTH
110+
);
111+
int currentHealth = previousHealth - 1;
112+
if (currentHealth > 0) {
113+
pdc.set(this.crystalHealthNsKey, PersistentDataType.INTEGER, currentHealth);
114+
Util.HealthIndicators.update(crystal, crystalHealthNsKey, END_CRYSTAL_MAX_HEALTH);
115+
event.setCancelled(true); // Preserve the crystal entity
116+
}
117+
}
118+
119+
public void init() {
120+
this.plugin.log.info("features: EndCrystalBuff: Initializing");
121+
Bukkit.getPluginManager().registerEvents(this, this.plugin);
122+
}
123+
}

src/main/java/de/kiridevs/ksmpplugin/main/KiriSmpPlugin.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package de.kiridevs.ksmpplugin.main;
22

3+
import de.kiridevs.ksmpplugin.features.DragonBuff;
4+
import de.kiridevs.ksmpplugin.features.EndCrystalBuff;
35
import de.kiridevs.ksmpplugin.recipes.*;
46
import io.papermc.paper.plugin.configuration.PluginMeta;
5-
import java.util.logging.Logger;
67
import org.bukkit.configuration.ConfigurationSection;
78
import org.bukkit.plugin.java.JavaPlugin;
89

10+
import java.util.logging.Logger;
11+
912
public class KiriSmpPlugin extends JavaPlugin {
1013

1114
public final Logger log;
@@ -34,12 +37,18 @@ private void registerRecipes() {
3437
new Woodcutter(this, stonecuttingConfig).register();
3538
}
3639

40+
public void initFeatures() {
41+
new DragonBuff(this).init();
42+
new EndCrystalBuff(this).init();
43+
}
44+
3745
@Override
3846
public void onEnable() {
3947
final PluginMeta pluginYml = this.getPluginMeta();
4048
this.log.info("Mom says I'm version " + pluginYml.getVersion() + "!");
4149
this.saveDefaultConfig();
4250

4351
registerRecipes();
52+
initFeatures();
4453
}
4554
}

0 commit comments

Comments
 (0)