more loyal 1.8 damage, invulnerability
This commit is contained in:
1 parent
8ab335d383
commit
3ce1e71695
5 files changed
+287
-11
No files matched your search
+162
@@ -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)
|
||||
|
||||
+18
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user