diff --git a/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java b/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java index 5757107d..d880c468 100644 --- a/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java +++ b/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java @@ -56,6 +56,11 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException public static double COMBAT_SHIELD_BLOCK_BONUS_DAMAGE_MOD; public static int COMBAT_SHIELD_BLOCK_COOLDOWN; public static boolean LEGACY_COMBAT; + public static String DEFENSE_FORMULA_MODE; + public static String CUSTOM_DEFENSE_FORMULA; + public static boolean COMBAT_OVERFLOW_PEN_AMPLIFIES; + public static String COMBAT_OVERFLOW_PEN_FORMULA; + public static boolean FULL_LEGACY; public static boolean COMBAT_DISABLE_VANILLA_SWEEP; public static boolean COMBAT_REDUCE_PLAYER_HEALTH_BAR; public static boolean COMBAT_FISHING_HOOK_DO_DAMAGE; @@ -208,6 +213,10 @@ public void setup() { path = "combat."; EngineCfg.LEGACY_COMBAT = cfg.getBoolean(path + "legacy-combat", false); + EngineCfg.DEFENSE_FORMULA_MODE = cfg.getString(path + "defense-formula", "FACTOR").toUpperCase(); + EngineCfg.CUSTOM_DEFENSE_FORMULA = cfg.getString(path + "custom-defense-formula", "damage*(25/(25+defense))"); + EngineCfg.COMBAT_OVERFLOW_PEN_AMPLIFIES = cfg.getBoolean(path + "overflow-pen-amplifies", false); + EngineCfg.COMBAT_OVERFLOW_PEN_FORMULA = cfg.getString(path + "overflow-pen-formula", "damage*(overflow/100)"); EngineCfg.COMBAT_DISABLE_VANILLA_SWEEP = cfg.getBoolean(path + "disable-vanilla-sweep-attack"); EngineCfg.COMBAT_REDUCE_PLAYER_HEALTH_BAR = cfg.getBoolean(path + "compress-player-health-bar"); EngineCfg.COMBAT_FISHING_HOOK_DO_DAMAGE = cfg.getBoolean(path + "fishing-hook-do-damage"); diff --git a/src/main/java/studio/magemonkey/divinity/manager/EntityManager.java b/src/main/java/studio/magemonkey/divinity/manager/EntityManager.java index ddc45f9f..46390700 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/EntityManager.java +++ b/src/main/java/studio/magemonkey/divinity/manager/EntityManager.java @@ -19,6 +19,7 @@ import studio.magemonkey.divinity.api.event.DivinityDamageEvent; import studio.magemonkey.divinity.api.event.EntityDivinityItemPickupEvent; import studio.magemonkey.divinity.api.event.EntityEquipmentChangeEvent; +import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.modules.api.QModuleDrop; import studio.magemonkey.divinity.stats.EntityStats; import studio.magemonkey.divinity.stats.EntityStatsTask; @@ -90,6 +91,7 @@ public void shutdown() { @EventHandler(priority = EventPriority.MONITOR) public void onStatsDeath(EntityDeathEvent e) { + if(EngineCfg.FULL_LEGACY) return; LivingEntity entity = e.getEntity(); previousEquipment.remove(e.getEntity().getUniqueId()); EntityStats.get(entity).handleDeath(); @@ -98,17 +100,20 @@ public void onStatsDeath(EntityDeathEvent e) { // Clear stats on player exit @EventHandler(priority = EventPriority.HIGHEST) public void onStatsQuit(PlayerQuitEvent e) { + if(EngineCfg.FULL_LEGACY) return; EntityStats.purge(e.getPlayer()); } @EventHandler(priority = EventPriority.HIGHEST) public void onStatsJoin(PlayerJoinEvent e) { + if(EngineCfg.FULL_LEGACY) return; EntityStats.get(e.getPlayer()); this.pushToUpdate(e.getPlayer(), 1D); } @EventHandler public void quit(PlayerQuitEvent event) { + if(EngineCfg.FULL_LEGACY) return; previousEquipment.remove(event.getPlayer().getUniqueId()); } @@ -124,6 +129,7 @@ public void onStatsRegen(EntityRegainHealthEvent e) { @EventHandler(ignoreCancelled = true) public void onPickup(EntityPickupItemEvent e) { + if(EngineCfg.FULL_LEGACY) return; if (!ProjectileStats.isPickable(e.getItem())) { e.setCancelled(true); } @@ -138,6 +144,7 @@ public void onPickup(EntityPickupItemEvent e) { } private final void pushToUpdate(@NotNull LivingEntity entity, double time) { + if(EngineCfg.FULL_LEGACY) return; EntityEquipment equip = new EntityEquipmentSnapshot(entity); previousEquipment.put(entity.getUniqueId(), equip); if (time <= 0D) { @@ -153,6 +160,7 @@ public void run() { } private final void addDuplicatorFixer(@NotNull Entity entity) { + if(EngineCfg.FULL_LEGACY) return; entity.setMetadata(PACKET_DUPLICATOR_FIXER, new FixedMetadataValue(plugin, "fixed")); } diff --git a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java index c1a3b290..e298f0d6 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java +++ b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import studio.magemonkey.codex.api.items.PrefixHelper; +import studio.magemonkey.codex.util.eval.Evaluator; import studio.magemonkey.codex.hooks.Hooks; import studio.magemonkey.codex.manager.IListener; import studio.magemonkey.codex.registry.provider.DamageTypeProvider; @@ -46,6 +47,8 @@ import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat; import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat; import studio.magemonkey.divinity.stats.items.attributes.stats.BleedStat; +import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat; +import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat; import java.util.*; import java.util.function.DoubleUnaryOperator; @@ -251,24 +254,119 @@ public void onDamageRPGStart(@NotNull DivinityDamageEvent.Start e) { if (!e.isExempt()) dmgType *= powerMod; dmgType *= blockMod; + // Apply damage buff % from attacker's equipment + if (statsDamager != null && dmgAtt != null) { + for (DynamicBuffStat buff : ItemStats.getDamageBuffs()) { + if (buff.isApplicableTo(dmgAtt.getId())) { + double buffPct = statsDamager.getDynamicBuff(buff); + if (buffPct != 0) dmgType *= (1.0 + buffPct / 100.0); + } + } + } + // Per-type penetration (PenetrationStat from penetration.yml) + // perTypePenMod: additional % pen multiplier (all formulas) + // perTypeFlatPen: flat defense reduction (CUSTOM formula only) + double perTypePenMod = 1.0; + double perTypeFlatPen = 0.0; + if (statsDamager != null && dmgAtt != null) { + for (PenetrationStat penStat : ItemStats.getPenetrations()) { + if (penStat.isApplicableTo(dmgAtt.getId())) { + double penValue = statsDamager.getPenetration(penStat); + if (penValue != 0) { + if (penStat.isPercentPen()) { + perTypePenMod *= Math.max(0D, 1.0 - penValue / 100.0); + } else { + perTypeFlatPen += penValue; + } + } + } + } + } double directType = dmgType * directMod; // Get direct value for this Damage Attribute dmgType = Math.max(0, dmgType - directType); // Deduct this value from damage if (dmgType > 0) { - DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; - if (defAtt != null && defenses.containsKey(defAtt)) { - double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod); - - double defCalced; - if (EngineCfg.LEGACY_COMBAT) { - defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); - } else { - defCalced = Math.max(0, + if (EngineCfg.LEGACY_COMBAT) { + // Legacy: 1:1, highest priority defense only + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (defAtt != null && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + // Apply defense buff % from victim's equipment + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(defAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); + } + } + double defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); + meta.setDefendedDamage(defAtt, dmgType - defCalced); + dmgType = defCalced; + } + } else if ("CUSTOM".equals(EngineCfg.DEFENSE_FORMULA_MODE)) { + // Custom: collect ALL matching defenses (group sum + individual placeholders) + double totalDef = 0; + Map individualDefs = new HashMap<>(); + for (DefenseAttribute defAtt : ItemStats.getDefenses()) { + if (dmgAtt != null && defAtt.isBlockable(dmgAtt) && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + totalDef += def; + individualDefs.put(defAtt.getId(), def); + } + } + // Apply defense buff % from victim's equipment (on summed total) + if (totalDef > 0 && dmgAtt != null) { + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(dmgAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) totalDef *= (1.0 + buffPct / 100.0); + } + } + } + // Apply flat penetration to total defense (CUSTOM formula only) + double overflowFlatPen = 0; + double preFlatPenDefVal = totalDef; // saved for overflow formula's 'defense' placeholder + if (perTypeFlatPen > 0) { + totalDef = Math.max(0, totalDef - perTypeFlatPen); + if (EngineCfg.COMBAT_OVERFLOW_PEN_AMPLIFIES && perTypeFlatPen > preFlatPenDefVal) { + overflowFlatPen = perTypeFlatPen - preFlatPenDefVal; + } + } + if (totalDef > 0) { + double defCalced = Math.max(0, evaluateDefenseFormula( + EngineCfg.CUSTOM_DEFENSE_FORMULA, dmgType, totalDef, toughness, individualDefs)); + DefenseAttribute primaryDef = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (primaryDef != null) { + meta.setDefendedDamage(primaryDef, dmgType - defCalced); + } + dmgType = defCalced; + } + // Apply flat pen overflow amplification + if (overflowFlatPen > 0) { + // defense = original totalDef before flat pen; overflow = flatPen - defense + double bonus = evaluateOverflowFormula( + EngineCfg.COMBAT_OVERFLOW_PEN_FORMULA, dmgType, overflowFlatPen, preFlatPenDefVal); + if (bonus > 0) { + dmgType += bonus; + } + } + } else { + // Factor: 1:1, highest priority defense only (minecraft formula) + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (defAtt != null && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + // Apply defense buff % from victim's equipment + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(defAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); + } + } + double defCalced = Math.max(0, dmgType * (1 - Math.max(def / 5, def - 4 * dmgType / Math.max(1, toughness + 8)) * defAtt.getProtectionFactor() * 0.05)); + meta.setDefendedDamage(defAtt, dmgType - defCalced); + dmgType = defCalced; } - meta.setDefendedDamage(defAtt, dmgType - defCalced); - dmgType = defCalced; } } //Should we reactivate direct damage, remove directType here and deal the damage straight. @@ -292,15 +390,13 @@ public void onDamageRPGStart(@NotNull DivinityDamageEvent.Start e) { // Compare modified damage and invulnerable prot. If they're within 0.0001 of each other, set the damage to 0. // and cancel the event if (modifiedDamage + invulnerableProt < 0.001) { - if (e.getOriginalEvent().getEntity().getType() != EntityType.ARMOR_STAND) { - e.setCancelled(true); - e.getOriginalEvent().setCancelled(true); - } + e.setCancelled(true); + e.getOriginalEvent().setCancelled(true); return; } meta.setInvulnerableProtection(invulnerableProt); - double dmgTotal = meta.getTotalDamage(); + double dmgTotal = Math.round(meta.getTotalDamage() * 100.0) / 100.0; // Divinity.getInstance().getLogger().info("Damage total: " + dmgTotal); // Divinity.getInstance().getLogger().info("Defended: " + meta.getDefendedDamage()); orig.setDamage(DamageModifier.BASE, dmgTotal); @@ -346,13 +442,30 @@ private boolean handleDamageModifiers( } } + // SKILL_CRITICAL_RATE/DAMAGE apply only to skill damage (e.g. Fabled DamageMechanic). + // Autoattacks (melee, projectile) use only the standard CRITICAL_RATE/DAMAGE. + FabledHook skillApiHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + boolean isSkillHit = skillApiHook != null && skillApiHook.isSkillDamage(); + double critRate = 0D; + if (!meta.isIgnoreCrit()) { + critRate += event.getDamagerItemStat(TypedStat.Type.CRITICAL_RATE); + } + if (isSkillHit && !meta.isIgnoreSkillCrit()) { + critRate += event.getDamagerItemStat(TypedStat.Type.SKILL_CRITICAL_RATE); + } double critModifier = 1D; - double critRate = event.getDamagerItemStat(TypedStat.Type.CRITICAL_RATE); if (critRate > 0 && Rnd.get(true) < critRate) { - critModifier = event.getDamagerItemStat(TypedStat.Type.CRITICAL_DAMAGE); - if (critModifier == 0D) { - critModifier = 1D; + // CRITICAL_DAMAGE has implicit 1.0 baseline (see EntityStats:950); strip it so both stats + // act as additive bonuses on top of the shared 1.0 baseline. Prevents critModifier < 1.0 + // when divinity-ignore-crit is set and only SKILL_CRITICAL_DAMAGE (< 1.0) contributes. + double critBonus = 0D; + if (!meta.isIgnoreCrit()) { + critBonus += event.getDamagerItemStat(TypedStat.Type.CRITICAL_DAMAGE) - 1D; + } + if (isSkillHit && !meta.isIgnoreSkillCrit()) { + critBonus += event.getDamagerItemStat(TypedStat.Type.SKILL_CRITICAL_DAMAGE); } + critModifier = 1D + Math.max(0D, critBonus); } meta.setCriticalModifier(critModifier); @@ -571,4 +684,37 @@ public void onDamage(DivinityDamageEvent.BeforeScale event) { } return success[0]; } + + static double evaluateDefenseFormula(String formula, double damage, double defense, + double toughness, Map individualDefs) { + String expr = formula + .replace("damage", String.valueOf(damage)) + .replace("toughness", String.valueOf(toughness)); + // Replace individual defense placeholders BEFORE the sum placeholder + // because "defense" is a prefix of "defense_" + for (Map.Entry entry : individualDefs.entrySet()) { + expr = expr.replace("defense_" + entry.getKey(), String.valueOf(entry.getValue())); + } + expr = expr.replace("defense", String.valueOf(defense)); + return Evaluator.eval(expr, 1); + } + + /** + * Evaluates the overflow pen formula and returns the bonus damage to add. + * + * @param formula the formula string from config (overflow-pen-formula) + * @param damage incoming damage after defense reduction (post-formula) + * @param overflow flat pen amount that exceeded the target's total defense + * @param defense total defense before flat pen was applied + * @return bonus damage to add; 0 on invalid result (NaN / Infinity / negative) + */ + static double evaluateOverflowFormula(String formula, double damage, + double overflow, double defense) { + String expr = formula + .replace("damage", String.valueOf(damage)) + .replace("overflow", String.valueOf(overflow)) + .replace("defense", String.valueOf(defense)); + double result = Evaluator.eval(expr, 1); + return Double.isFinite(result) ? Math.max(0, result) : 0; + } } diff --git a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageMeta.java b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageMeta.java index 10ee2cff..bc0be6b5 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageMeta.java +++ b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageMeta.java @@ -31,6 +31,8 @@ public class DamageMeta { private double pveDefModifier = 1D; private double directModifier = 0D; private double criticalModifier = 1D; + private boolean ignoreCrit = false; + private boolean ignoreSkillCrit = false; private double penetrateModifier = 1D; private double enchantProtectionModifier = 1D; @Getter @@ -208,6 +210,22 @@ public void setCriticalModifier(double critMod) { this.criticalModifier = critMod; } + public boolean isIgnoreCrit() { + return this.ignoreCrit; + } + + public void setIgnoreCrit(boolean ignoreCrit) { + this.ignoreCrit = ignoreCrit; + } + + public boolean isIgnoreSkillCrit() { + return this.ignoreSkillCrit; + } + + public void setIgnoreSkillCrit(boolean ignoreSkillCrit) { + this.ignoreSkillCrit = ignoreSkillCrit; + } + public double getDirectModifier() { return this.directModifier; } diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/ListenerManager.java b/src/main/java/studio/magemonkey/divinity/manager/listener/ListenerManager.java index dbb20fe1..94d65295 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/ListenerManager.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/ListenerManager.java @@ -1,9 +1,10 @@ package studio.magemonkey.divinity.manager.listener; import org.jetbrains.annotations.NotNull; -import studio.magemonkey.codex.core.Version; import studio.magemonkey.codex.manager.api.Loadable; import studio.magemonkey.divinity.Divinity; +import studio.magemonkey.divinity.config.Config; +import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.hooks.HookListener; import studio.magemonkey.divinity.manager.listener.object.*; import studio.magemonkey.divinity.stats.items.ItemStats; @@ -20,7 +21,6 @@ public class ListenerManager implements Loadable { private ItemUpdaterListener updater; private VanillaWrapperListener lisQuantum; private HookListener hookListener; - private GrindstoneListener grindstoneListener; public ListenerManager(@NotNull Divinity plugin) { this.plugin = plugin; @@ -47,19 +47,19 @@ public void setup() { this.lisDynamic = new DynamicStatListener(this.plugin); this.lisDynamic.registerListeners(); - this.lisQuantum = new VanillaWrapperListener(this.plugin); - this.lisQuantum.registerListeners(); + if(!EngineCfg.LEGACY_COMBAT) { + this.lisQuantum = new VanillaWrapperListener(this.plugin); + this.lisQuantum.registerListeners(); + Divinity.getInstance().getLogger().info("Loaded " + this.lisQuantum.getClass().getSimpleName()); + } else { + Divinity.getInstance().getLogger().info("Skipped " + VanillaWrapperListener.class.getSimpleName() + " due to legacy combat being enabled."); + } this.updater = new ItemUpdaterListener(this.plugin); this.updater.registerListeners(); this.hookListener = new HookListener(this.plugin); this.hookListener.registerListeners(); - - if (Version.CURRENT.isAtLeast(Version.V1_19_R3)) { - this.grindstoneListener = new GrindstoneListener(this.plugin); - this.grindstoneListener.registerListeners(); - } } @Override @@ -80,9 +80,5 @@ public void shutdown() { this.lisQuantum.unregisterListeners(); this.lisQuantum = null; } - if (this.grindstoneListener != null) { - this.grindstoneListener.unregisterListeners(); - this.grindstoneListener = null; - } } } diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/DynamicStatListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/DynamicStatListener.java index 2c2f021b..a79bafc8 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/DynamicStatListener.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/object/DynamicStatListener.java @@ -13,6 +13,7 @@ import org.jetbrains.annotations.Nullable; import studio.magemonkey.codex.manager.IListener; import studio.magemonkey.divinity.Divinity; +import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.stats.items.ItemStats; import studio.magemonkey.divinity.stats.items.api.DynamicStat; @@ -27,6 +28,7 @@ public DynamicStatListener(@NotNull Divinity plugin) { } public static void updateItem(@Nullable Player p, @NotNull ItemStack item) { + if(EngineCfg.FULL_LEGACY) return; for (DynamicStat dynamicStat : ItemStats.getDynamicStats()) { dynamicStat.updateItem(p, item); } @@ -34,12 +36,14 @@ public static void updateItem(@Nullable Player p, @NotNull ItemStack item) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onDrop(PlayerDropItemEvent e) { + if(EngineCfg.FULL_LEGACY) return; ItemStack item = e.getItemDrop().getItemStack(); updateItem(null, item); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPick(EntityPickupItemEvent e) { + if(EngineCfg.FULL_LEGACY) return; LivingEntity entity = e.getEntity(); if (!(entity instanceof Player)) return; @@ -50,6 +54,7 @@ public void onPick(EntityPickupItemEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onInvOpen(InventoryOpenEvent e) { + if(EngineCfg.FULL_LEGACY) return; List list = new ArrayList<>(); Player player = (Player) e.getPlayer(); @@ -64,6 +69,7 @@ public void onInvOpen(InventoryOpenEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onInvClose(InventoryCloseEvent e) { + if(EngineCfg.FULL_LEGACY) return; Player player = (Player) e.getPlayer(); List list = new ArrayList<>(); diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/GrindstoneListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/GrindstoneListener.java deleted file mode 100644 index a8bdf460..00000000 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/GrindstoneListener.java +++ /dev/null @@ -1,23 +0,0 @@ -package studio.magemonkey.divinity.manager.listener.object; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.PrepareGrindstoneEvent; -import org.bukkit.inventory.ItemStack; -import org.jspecify.annotations.NonNull; -import studio.magemonkey.codex.manager.IListener; -import studio.magemonkey.divinity.Divinity; -import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; - -public class GrindstoneListener extends IListener { - public GrindstoneListener(@NonNull Divinity plugin) { - super(plugin); - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onGrindStone(PrepareGrindstoneEvent e) { - ItemStack result = e.getResult(); - if (result == null) return; - ItemGeneratorManager.updateGeneratorItemLore(result); - } -} diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemUpdaterListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemUpdaterListener.java index b21b19e9..7589a4a5 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemUpdaterListener.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemUpdaterListener.java @@ -25,6 +25,7 @@ import studio.magemonkey.codex.manager.IListener; import studio.magemonkey.codex.util.DataUT; import studio.magemonkey.divinity.Divinity; +import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.stats.items.ItemStats; public class ItemUpdaterListener extends IListener { @@ -93,6 +94,7 @@ public void join(PlayerJoinEvent event) { } public void update(ItemStack item, @Nullable Player player) { + if(EngineCfg.LEGACY_COMBAT ||EngineCfg.FULL_LEGACY) return; if (item == null || item.getType() == Material.AIR) return; ItemType itemType = CodexEngine.get().getItemManager().getMainItemType(item); diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java index e609e643..8400c849 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java @@ -13,6 +13,7 @@ import org.bukkit.event.entity.EntityShootBowEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.bukkit.event.inventory.PrepareGrindstoneEvent; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.FixedMetadataValue; @@ -67,7 +68,7 @@ public void onVanillaShootBow(EntityShootBowEvent e) { Vector orig = pj.getVelocity(); double power = e.getForce(); - if (Version.CURRENT.isAtLeast(Version.V1_20_R4) && Version.CURRENT.isLower(Version.V1_21_R4)) { + if (Version.CURRENT.isAtLeast(Version.V1_20_R4)) { power /= 3; } @@ -166,6 +167,7 @@ public void onVanillaDamage(EntityDamageEvent e) { EntityStats statsDamager = null; EntityStats statsVictim = EntityStats.get(victim); + if (!(victim instanceof Player)) statsVictim.updateInventory(); DamageMeta meta = new DamageMeta(victim, damager, weapon, cause); statsVictim.setLastDamageMeta(meta); @@ -196,6 +198,7 @@ public void onVanillaDamage(EntityDamageEvent e) { meta.setDamager(damager); statsDamager = EntityStats.get(damager); + if (!(damager instanceof Player)) statsDamager.updateInventory(); statsDamager.setLastDamageMeta(meta); weapon = statsDamager.getItemInMainHand(); @@ -218,6 +221,7 @@ public void onVanillaDamage(EntityDamageEvent e) { damager = (LivingEntity) shooter; meta.setDamager(damager); statsDamager = EntityStats.get(damager); + if (!(damager instanceof Player)) statsDamager.updateInventory(); statsDamager.setLastDamageMeta(meta); weapon = ProjectileStats.getSrcWeapon(projectile); @@ -503,4 +507,11 @@ public void onEnchantingTable(PrepareItemEnchantEvent e) { ItemStack result = e.getItem(); ItemGeneratorManager.updateGeneratorItemLore(result); } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onGrindStone(PrepareGrindstoneEvent e) { + ItemStack result = e.getResult(); + if (result == null) return; + ItemGeneratorManager.updateGeneratorItemLore(result); + } } diff --git a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java index 01f8e9ec..2783f93c 100644 --- a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java +++ b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java @@ -413,7 +413,7 @@ public synchronized List getEquipment() { return new ArrayList<>(this.inventory); } - private void updateInventory() { + public void updateInventory() { this.inventory.clear(); ItemStack[] armor = new ItemStack[0]; @@ -440,6 +440,7 @@ private void updateInventory() { } public void updateAll() { + if(EngineCfg.LEGACY_COMBAT) return; if (!EngineCfg.ATTRIBUTES_EFFECTIVE_FOR_MOBS && !this.isPlayer()) { return; } diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java b/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java index 2f177b96..59aa4954 100644 --- a/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java +++ b/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java @@ -1,6 +1,5 @@ package studio.magemonkey.divinity.stats.items; -import org.bukkit.Bukkit; import org.bukkit.Keyed; import org.bukkit.NamespacedKey; import org.bukkit.attribute.Attribute; @@ -20,6 +19,7 @@ import studio.magemonkey.codex.modules.IModule; import studio.magemonkey.codex.util.DataUT; import studio.magemonkey.divinity.Divinity; +import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.modules.api.QModuleDrop; import studio.magemonkey.divinity.stats.items.api.DuplicableItemLoreStat; import studio.magemonkey.divinity.stats.items.api.DynamicStat; @@ -358,13 +358,12 @@ public static boolean hasStat(@NotNull ItemStack item, @Nullable Player player, // ----------------------------------------------------------------- // public static void updateVanillaAttributes(@NotNull ItemStack item, @Nullable Player player) { + if(EngineCfg.FULL_LEGACY || EngineCfg.LEGACY_COMBAT) return; + addAttribute(item, player, NBTAttribute.MAX_HEALTH, getStat(item, player, TypedStat.Type.MAX_HEALTH)); addAttribute(item, player, NBTAttribute.MOVEMENT_SPEED, getStat(item, player, TypedStat.Type.MOVEMENT_SPEED)); addAttribute(item, player, NBTAttribute.ATTACK_SPEED, getStat(item, player, TypedStat.Type.ATTACK_SPEED)); - addAttribute(item, - player, - NBTAttribute.KNOCKBACK_RESISTANCE, - getStat(item, player, TypedStat.Type.KNOCKBACK_RESISTANCE)); + addAttribute(item, player, NBTAttribute.KNOCKBACK_RESISTANCE, getStat(item, player, TypedStat.Type.KNOCKBACK_RESISTANCE)); double vanilla = DamageAttribute.getVanillaDamage(item); if (vanilla > 1) addAttribute(item, player, NBTAttribute.ATTACK_DAMAGE, vanilla); @@ -378,16 +377,12 @@ public static void updateVanillaAttributes(@NotNull ItemStack item, @Nullable Pl toughness == 0 ? DefenseAttribute.getVanillaToughness(item) : toughness); } ItemMeta im = item.getItemMeta(); - if (im == null) { - im = Bukkit.getItemFactory().getItemMeta(item.getType()); - } // For 1.20.4+, the HIDE_ATTRIBUTES flag doesn't work unless an attribute has been added that's not the default. // Note: This only applies to Paper and its forks. if (Version.CURRENT.isAtLeast(Version.V1_20_R4)) { Attribute moveSpeed = VersionManager.getNms().getAttribute("MOVEMENT_SPEED"); - if (!im.hasAttributeModifiers() - || im.getAttributeModifiers(VersionManager.getNms().getAttribute("MOVEMENT_SPEED")) == null) { + if (im.getAttributeModifiers(VersionManager.getNms().getAttribute("MOVEMENT_SPEED")) == null) { //noinspection RedundantCast im.addAttributeModifier(moveSpeed, new AttributeModifier(((Keyed) moveSpeed).getKey().getKey(), 0, Operation.ADD_NUMBER)); @@ -402,6 +397,7 @@ private static void addAttribute(@NotNull ItemStack item, @Nullable Player player, @NotNull NBTAttribute att, double value) { + //if(EngineCfg.LEGACY_COMBAT) return; ItemMeta meta = item.getItemMeta(); if (meta == null) return; diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java index 2e8bc279..d955ceee 100644 --- a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java +++ b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java @@ -70,6 +70,8 @@ enum Type { PVE_DEFENSE(SimpleStat.ItemType.BOTH, true, true, true), CRITICAL_RATE(SimpleStat.ItemType.BOTH, true, true, true), CRITICAL_DAMAGE(SimpleStat.ItemType.BOTH, false, false, true), + SKILL_CRITICAL_RATE(SimpleStat.ItemType.BOTH, true, true, true), + SKILL_CRITICAL_DAMAGE(SimpleStat.ItemType.BOTH, false, false, true), DURABILITY(SimpleStat.ItemType.BOTH, false, true, false), PENETRATION(SimpleStat.ItemType.BOTH, true, true, true), VAMPIRISM(SimpleStat.ItemType.BOTH, true, true, true), diff --git a/src/main/resources/engine.yml b/src/main/resources/engine.yml index 81ca70bf..bac83ee7 100644 --- a/src/main/resources/engine.yml +++ b/src/main/resources/engine.yml @@ -67,6 +67,28 @@ attributes: combat: # Whether to use the old combat formula for calculating defenses legacy-combat: false + # Defense formula. Only used when legacy-combat is false. + # Options: FACTOR (minecraft formula), CUSTOM + defense-formula: FACTOR + # Custom defense formula. Only used when defense-formula is CUSTOM. + # Placeholders: damage, defense (sum of all matching defenses after all penetration), + # defense_ (individual defense, e.g. defense_weapon, defense_physical), + # toughness + # NOTE: 'defense' is ALWAYS >= 0 here. Penetration cannot make defense go negative — + # all defense values are clamped to 0 before being passed to this formula. + # In CUSTOM mode, ALL defenses with matching block-damage-types are summed. + # In FACTOR mode, only the highest priority defense is used (1:1). + custom-defense-formula: 'damage*(25/(25+defense))' + # If true, flat penetration that exceeds the target's total defense amplifies damage. + # Example: flatPen=100, defense=30 → overflow=70 → bonus damage via overflow-pen-formula. + # Only applies in CUSTOM defense formula mode. + overflow-pen-amplifies: false + # Formula for bonus damage when flat pen overflow occurs (overflow-pen-amplifies must be true). + # Placeholders: damage (incoming damage before defense), overflow (flatPen - defense), + # defense (total defense before flat pen was applied) + # Result is added on top of the post-defense damage. + # Tune this for your server's typical defense and flat pen values. + overflow-pen-formula: 'damage*(overflow/100)' # Shield settings. shield: block: diff --git a/src/test/java/studio/magemonkey/divinity/manager/damage/DefenseFormulaEvaluationTest.java b/src/test/java/studio/magemonkey/divinity/manager/damage/DefenseFormulaEvaluationTest.java new file mode 100644 index 00000000..db777dbf --- /dev/null +++ b/src/test/java/studio/magemonkey/divinity/manager/damage/DefenseFormulaEvaluationTest.java @@ -0,0 +1,60 @@ +package studio.magemonkey.divinity.manager.damage; + +import org.junit.jupiter.api.Test; +import studio.magemonkey.divinity.testutil.MockedTest; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Covers the CUSTOM defense-formula string evaluation (DamageManager#evaluateDefenseFormula / + * #evaluateOverflowFormula), package-visible specifically so these pure-logic pieces can be + * exercised directly without a full combat event simulation. + */ +class DefenseFormulaEvaluationTest extends MockedTest { + + @Test + void evaluateDefenseFormula_defaultFormula_appliesFactorStyleReduction() { + // Default engine.yml formula: damage*(25/(25+defense)) + double result = DamageManager.evaluateDefenseFormula( + "damage*(25/(25+defense))", 100D, 25D, 0D, Map.of()); + + assertEquals(50D, result, 0.001); + } + + @Test + void evaluateDefenseFormula_zeroDefense_dealsFullDamage() { + double result = DamageManager.evaluateDefenseFormula( + "damage*(25/(25+defense))", 100D, 0D, 0D, Map.of()); + + assertEquals(100D, result, 0.001); + } + + @Test + void evaluateDefenseFormula_individualDefensePlaceholder_substitutedBeforeSumPlaceholder() { + // "defense" is a prefix of "defense_physical", so the individual placeholder must be + // substituted first or this formula would break. + double result = DamageManager.evaluateDefenseFormula( + "damage - defense_physical", 100D, 999D, 0D, Map.of("physical", 30D)); + + assertEquals(70D, result, 0.001); + } + + @Test + void evaluateOverflowFormula_defaultFormula_scalesOverflowByDamage() { + // Default engine.yml formula: damage*(overflow/100) + double result = DamageManager.evaluateOverflowFormula( + "damage*(overflow/100)", 50D, 20D, 10D); + + assertEquals(10D, result, 0.001); + } + + @Test + void evaluateOverflowFormula_negativeResult_clampsToZero() { + double result = DamageManager.evaluateOverflowFormula( + "damage - 1000", 10D, 20D, 5D); + + assertEquals(0D, result, 0.001); + } +}