Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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());
}

Expand All @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, Double> 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.
Expand All @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<String, Double> 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_<id>"
for (Map.Entry<String, Double> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
Loading