more loyal 1.8 damage, invulnerability

This commit is contained in:
NotAlexNoyle committed 2026-08-10 07:49:05 -04:00
1 parent 8ab335d383
commit 3ce1e71695
5 files changed
+287 -11

No files matched your search

@@ -6,20 +6,26 @@
package kernitus.plugin.OldCombatMechanics
import com.cryptomorin.xseries.XAttribute
import io.kotest.common.ExperimentalKotest
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.doubles.plusOrMinus
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
import io.kotest.matchers.shouldBe
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities
import kernitus.plugin.OldCombatMechanics.utilities.Config
import kernitus.plugin.OldCombatMechanics.utilities.damage.NewWeaponDamage
import kotlinx.coroutines.delay
import org.bukkit.Bukkit
import org.bukkit.Difficulty
import org.bukkit.GameMode
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.entity.AbstractArrow
import org.bukkit.entity.Player
import org.bukkit.entity.Skeleton
import org.bukkit.entity.Trident
import org.bukkit.entity.Zombie
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
@@ -413,6 +419,162 @@ class MobInvulnerabilityIntegrationTest :
}
}
test("in-window thrown trident deals the configured value minus the baseline") {
if (!CompatibilityCapabilities.isBukkitClassAvailable("org.bukkit.entity.Trident")) {
plugin.logger.info("MobInvuln[trident-window]: no Trident class, skipping")
return@test
}
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
val configured = 15.0
val baseline = 2.0
val oldTrident = ocm.config.get("old-tool-damage.damages.TRIDENT_THROWN")
lateinit var victimFake: FakePlayer
lateinit var victim: Player
var landed = -1.0
var tick = 0
val listener = object : Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
fun onDamage(event: EntityDamageByEntityEvent) {
if (event.entity.uniqueId == victim.uniqueId && event.damager is Trident) {
landed = event.finalDamage
}
}
}
try {
runSync {
ocm.config.set("old-tool-damage.damages.TRIDENT_THROWN", configured)
ocm.saveConfig()
Config.reload()
val world = checkNotNull(Bukkit.getWorld("world"))
val base = Location(world, 70.0, 100.0, 70.0)
victimFake = FakePlayer(plugin)
victimFake.spawn(base)
victim = checkNotNull(Bukkit.getPlayer(victimFake.uuid))
victim.gameMode = GameMode.SURVIVAL
victim.isInvulnerable = false
victim.inventory.clear()
victim.inventory.armorContents = arrayOfNulls(4)
victim.health = victim.maxHealth
victim.noDamageTicks = 0
victim.lastDamage = 0.0
Bukkit.getPluginManager().registerEvents(listener, plugin)
object : BukkitRunnable() {
override fun run() {
tick += 1
if (tick > 8) {
cancel()
return
}
if (tick == 1) {
// Non-entity damage opens the window and leaves a real NMS baseline
victim.damage(baseline)
} else if (tick == 3) {
val eye = victim.location.clone().add(0.0, 1.2, 0.0)
val from = eye.clone().add(0.9, 0.0, 0.0)
val dir = eye.toVector().subtract(from.toVector()).normalize()
val trident = world.spawn(from, Trident::class.java)
trident.velocity = dir.multiply(0.6)
}
}
}.runTaskTimer(plugin, 1L, 1L)
}
while (tick <= 8) delayTicks(4)
delayTicks(3)
plugin.logger.info("MobInvuln[trident-window]: landed=$landed expected=${configured - baseline}")
// The configured value is a full hit, so the window must subtract the baseline once.
// Adding the baseline back on top of a config constant would deal the full value.
landed shouldBe ((configured - baseline) plusOrMinus 0.001)
} finally {
HandlerList.unregisterAll(listener)
runSync {
ocm.config.set("old-tool-damage.damages.TRIDENT_THROWN", oldTrident)
ocm.saveConfig()
Config.reload()
victimFake.removePlayer()
victim.world.entities.filterIsInstance<AbstractArrow>().forEach { it.remove() }
}
}
}
test("armed mob weapon delta scales with hard difficulty") {
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
lateinit var victimFake: FakePlayer
lateinit var victim: Player
lateinit var zombie: Zombie
var landed = -1.0
val world = runSync { checkNotNull(Bukkit.getWorld("world")) }
val oldDifficulty = runSync { world.difficulty }
val configuredIronAxe = ocm.config.getDouble("old-tool-damage.damages.IRON_AXE", -1.0)
val listener = object : Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
fun onDamage(event: EntityDamageByEntityEvent) {
if (event.entity.uniqueId == victim.uniqueId && event.damager is Zombie) {
landed = event.finalDamage
}
}
}
try {
if (configuredIronAxe <= 0.0) {
plugin.logger.info("MobInvuln[hard-delta]: no IRON_AXE damage configured, skipping")
return@test
}
runSync {
world.difficulty = Difficulty.HARD
victimFake = FakePlayer(plugin)
victimFake.spawn(Location(world, 60.0, 100.0, 60.0))
victim = checkNotNull(Bukkit.getPlayer(victimFake.uuid))
victim.gameMode = GameMode.SURVIVAL
victim.isInvulnerable = false
victim.inventory.clear()
victim.inventory.armorContents = arrayOfNulls(4)
victim.health = victim.maxHealth
victim.noDamageTicks = 0
victim.lastDamage = 0.0
zombie = world.spawn(Location(world, 61.0, 100.0, 60.0), Zombie::class.java)
zombie.setAI(false)
zombie.isSilent = true
zombie.equipment?.setItemInMainHand(ItemStack(Material.IRON_AXE))
Bukkit.getPluginManager().registerEvents(listener, plugin)
}
delayTicks(2)
val attribute = runSync {
val attackDamage = checkNotNull(XAttribute.ATTACK_DAMAGE.get())
checkNotNull(zombie.getAttribute(attackDamage)).value
}
// Bukkit damage() drives Player.hurt, so hard difficulty scales this by 1.5 first
runSync { victim.damage(attribute, zombie) }
delayTicks(3)
val vanillaItem = NewWeaponDamage.getDamageOrNull(Material.IRON_AXE)?.toDouble()
val delta = configuredIronAxe - checkNotNull(vanillaItem)
val expected = (attribute + delta) * 1.5
plugin.logger.info(
"MobInvuln[hard-delta]: attribute=$attribute delta=$delta landed=$landed expected=$expected"
)
// The delta must be scaled with the hit, so the result equals the 1.8 value times 1.5
landed shouldBe (expected plusOrMinus 0.001)
} finally {
HandlerList.unregisterAll(listener)
runSync {
world.difficulty = oldDifficulty
victimFake.removePlayer()
zombie.remove()
}
}
}
test("missing attack-frequency keys fall back to the vanilla 20-tick window") {
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
lateinit var victimFake: FakePlayer
@@ -16,6 +16,7 @@ import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
@@ -44,6 +45,7 @@ public class ModuleOldToolDamage extends OCMModule {
private boolean tooltipEnabled;
private String tooltipPrefix;
private final TooltipListener tooltipListener;
private static ModuleOldToolDamage INSTANCE;
static {
Class<?> tridentClass = null;
@@ -60,6 +62,7 @@ public class ModuleOldToolDamage extends OCMModule {
public ModuleOldToolDamage(OCMMain plugin) {
super(plugin, "old-tool-damage");
INSTANCE = this;
tooltipListener = new TooltipListener();
Bukkit.getPluginManager().registerEvents(tooltipListener, plugin);
reload();
@@ -125,7 +128,7 @@ public class ModuleOldToolDamage extends OCMModule {
event.setBaseDamage(adjustedBase);
Messenger.debug("Old tool damage: " + oldBaseDamage + " New: " + adjustedBase);
} else if (damager instanceof org.bukkit.entity.LivingEntity) {
} else if (damager instanceof LivingEntity) {
if (expectedBaseDamage == null) {
debug("No baseline damage for " + weaponMaterial + ", ignoring mob weapon.", damager);
return;
@@ -135,7 +138,9 @@ public class ModuleOldToolDamage extends OCMModule {
// This means custom mob weapons are not detected and will still be shifted, which may
// interact poorly with other plugins that modify mob damage in non-vanilla ways.
final double delta = newWeaponBaseDamage - expectedBaseDamage;
final double newBaseDamage = oldBaseDamage + delta;
// Difficulty scales incoming damage before the event fires (hard is 1.5x), so the delta
// must be scaled the same way or armed mobs keep a fraction of their 1.9 damage
final double newBaseDamage = oldBaseDamage + delta * incomingDamageScale(event.getDamagee(), oldBaseDamage);
event.setBaseDamage(newBaseDamage);
Messenger.debug("Old tool damage (mob): " + oldBaseDamage + " New: " + newBaseDamage);
}
@@ -163,16 +168,35 @@ public class ModuleOldToolDamage extends OCMModule {
return mat.toString().endsWith("_" + type.toUpperCase(Locale.ROOT));
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onTridentProjectile(org.bukkit.event.entity.EntityDamageByEntityEvent event) {
if (!HAS_TRIDENT || !TRIDENT_CLASS.isInstance(event.getDamager())) return;
if (!isEnabledForAttacker(event.getDamager())) return;
// Difficulty scaling that vanilla applies to player damage before the event fires, so weapon
// deltas expressed in unscaled 1.9 terms land on the same scale as the hit. Only players are
// scaled, and only for mob attackers, which is exactly this branch.
private double incomingDamageScale(Entity damagee, double scaledBaseDamage) {
if (!(damagee instanceof Player) || scaledBaseDamage <= 0) return 1.0;
switch (damagee.getWorld().getDifficulty()) {
case HARD:
return 1.5;
case EASY:
// Easy is min(amount / 2 + 1, amount), an affine map, so a delta scales by its
// slope of 0.5, and only above 2 damage where the halving actually applies
return scaledBaseDamage > 2.0 ? 0.5 : 1.0;
default:
return 1.0;
}
}
// Configured thrown-trident damage, or a negative value to leave the damage alone.
// EntityDamageByEntityListener applies this before the overdamage gate so the stored
// baseline matches the damage the hit actually deals.
public double getThrownTridentDamage(Entity damager) {
if (!HAS_TRIDENT || !TRIDENT_CLASS.isInstance(damager)) return -1;
if (!isEnabledForAttacker(damager)) return -1;
final double configured = WeaponDamages.getDamage("TRIDENT_THROWN");
if (configured <= 0) return;
return configured > 0 ? configured : -1;
}
event.setDamage(configured);
debug("Applied custom thrown trident damage: " + configured, event.getDamager());
public static ModuleOldToolDamage getInstance() {
return INSTANCE;
}
private boolean shouldApplyTooltip(Player player) {
@@ -18,13 +18,18 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.entity.VillagerAcquireTradeEvent;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.*;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.ItemMeta;
@@ -88,6 +93,18 @@ public class ModuleSwordBlocking extends OCMModule {
initialisePacketEventsClientVersion();
initialiseLegacyShieldMarker();
Bukkit.getPluginManager().registerEvents(new ConsumableLifecycleHandler(), plugin);
startShieldPurgeSweeper();
}
// Events cannot cover every way a shield appears, e.g. a staff /give or another plugin
// writing one straight into an inventory, so sweep online players as a backstop.
private void startShieldPurgeSweeper() {
Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (!removeRealShields()) return;
for (Player player : Bukkit.getOnlinePlayers()) {
purgeRealShields(player.getInventory());
}
}, 20L, 20L);
}
@Override
@@ -425,6 +442,60 @@ public class ModuleSwordBlocking extends OCMModule {
public void onPlayerJoin(PlayerJoinEvent e) {
restore(e.getPlayer(), true);
stripConsumableState(e.getPlayer(), true);
purgeRealShields(e.getPlayer().getInventory());
purgeRealShields(e.getPlayer().getEnderChest());
}
// Shields must not exist as items on this server, so the only one allowed is the marked
// temporary offhand shield this module creates for sword blocking. Everything else is removed.
private boolean removeRealShields() {
return module().getBoolean("remove-real-shields", true);
}
private boolean isRealShield(ItemStack item) {
if (item == null || item.getType() != Material.SHIELD) return false;
// Without marker support the temporary shield is indistinguishable, so keep every shield
if (!canMarkTemporaryLegacyShield()) return false;
return !hasTemporaryLegacyShieldMarker(item);
}
private void purgeRealShields(Inventory inventory) {
if (!removeRealShields() || inventory == null) return;
final ItemStack[] contents = inventory.getContents();
for (int slot = 0; slot < contents.length; slot++) {
// The marker alone decides, so a real shield parked in the offhand is removed too
if (isRealShield(contents[slot])) {
inventory.setItem(slot, null);
debug("Removed a shield: shields do not exist on this server");
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onVillagerAcquireTrade(VillagerAcquireTradeEvent e) {
if (!removeRealShields()) return;
final ItemStack result = e.getRecipe().getResult();
if (result != null && result.getType() == Material.SHIELD) e.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onShieldPickup(EntityPickupItemEvent e) {
if (!removeRealShields()) return;
if (!isRealShield(e.getItem().getItemStack())) return;
e.setCancelled(true);
e.getItem().remove();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onShieldItemSpawn(ItemSpawnEvent e) {
if (!removeRealShields()) return;
if (isRealShield(e.getEntity().getItemStack())) e.setCancelled(true);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onInventoryCloseStripShields(InventoryCloseEvent e) {
purgeRealShields(e.getInventory());
if (e.getPlayer() instanceof Player) purgeRealShields(((Player) e.getPlayer()).getInventory());
}
@EventHandler(priority = EventPriority.HIGHEST)
@@ -7,6 +7,7 @@ package kernitus.plugin.OldCombatMechanics.utilities.damage;
import kernitus.plugin.OldCombatMechanics.OCMMain;
import kernitus.plugin.OldCombatMechanics.module.OCMModule;
import kernitus.plugin.OldCombatMechanics.module.ModuleOldToolDamage;
import kernitus.plugin.OldCombatMechanics.module.ModuleSwordBlocking;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@@ -170,6 +171,16 @@ public class EntityDamageByEntityListener extends OCMModule {
// Rebuild the full vanilla amount: in-window NMS already subtracted gate-time lastHurt
if ((float) livingDamagee.getNoDamageTicks() > (float) livingDamagee.getMaximumNoDamageTicks() / 2.0F)
fullDamage += gateTimeLastHurt;
// Configured thrown-trident damage replaces the full amount before gating, so the
// stored baseline matches the hit. It is a full hit value, never an increment.
final ModuleOldToolDamage toolDamage = ModuleOldToolDamage.getInstance();
if (toolDamage != null) {
final double tridentDamage = toolDamage.getThrownTridentDamage(damager);
if (tridentDamage > 0) {
fullDamage = tridentDamage;
debug("Applied custom thrown trident damage: " + tridentDamage, damager);
}
}
final double gated = checkOverdamage(livingDamagee, event, fullDamage);
if (!event.isCancelled()) event.setDamage(Math.max(0.0, gated));
}
@@ -379,8 +390,13 @@ public class EntityDamageByEntityListener extends OCMModule {
}
// Update the last damage done, including when it was overdamage.
// This means attacks must keep increasing in value during immunity period to keep dealing overdamage.
lastDamages.put(livingDamagee.getUniqueId(), newLastDamage);
touchExpiry(livingDamagee);
// Non-entity damage outside the window is cleared again by the MONITOR handler below, so skip
// storing it here rather than storing a baseline that is deleted in the same event
if (event instanceof EntityDamageByEntityEvent
|| (float) livingDamagee.getNoDamageTicks() > (float) livingDamagee.getMaximumNoDamageTicks() / 2.0F) {
lastDamages.put(livingDamagee.getUniqueId(), newLastDamage);
touchExpiry(livingDamagee);
}
return newDamage;
}
+3
View File
@@ -247,6 +247,9 @@ sword-blocking:
restoreDelay: 40
# Whether to require players to have oldcombatmechanics.swordblock permission to block with a sword
use-permission: false
# Delete every shield that is not the temporary one this module creates for sword blocking
# Also blocks the armorer villager shield trade and shield pickups, so shields cannot be kept
remove-real-shields: true
disable-sword-sweep:
# This is to disable the sword sweep attack