|
23 | 23 | import org.jetbrains.annotations.NotNull; |
24 | 24 | import org.jetbrains.annotations.Nullable; |
25 | 25 | import studio.magemonkey.codex.api.items.PrefixHelper; |
| 26 | +import studio.magemonkey.codex.util.eval.Evaluator; |
26 | 27 | import studio.magemonkey.codex.hooks.Hooks; |
27 | 28 | import studio.magemonkey.codex.manager.IListener; |
28 | 29 | import studio.magemonkey.codex.registry.provider.DamageTypeProvider; |
|
46 | 47 | import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat; |
47 | 48 | import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat; |
48 | 49 | import studio.magemonkey.divinity.stats.items.attributes.stats.BleedStat; |
| 50 | +import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat; |
| 51 | +import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat; |
49 | 52 |
|
50 | 53 | import java.util.*; |
51 | 54 | import java.util.function.DoubleUnaryOperator; |
@@ -251,24 +254,105 @@ public void onDamageRPGStart(@NotNull DivinityDamageEvent.Start e) { |
251 | 254 | if (!e.isExempt()) |
252 | 255 | dmgType *= powerMod; |
253 | 256 | dmgType *= blockMod; |
| 257 | + // Apply damage buff % from attacker's equipment |
| 258 | + if (statsDamager != null && dmgAtt != null) { |
| 259 | + for (DynamicBuffStat buff : ItemStats.getDamageBuffs()) { |
| 260 | + if (buff.isApplicableTo(dmgAtt.getId())) { |
| 261 | + double buffPct = statsDamager.getDynamicBuff(buff); |
| 262 | + if (buffPct != 0) dmgType *= (1.0 + buffPct / 100.0); |
| 263 | + } |
| 264 | + } |
| 265 | + } |
| 266 | + // Per-type penetration (PenetrationStat from penetration.yml) |
| 267 | + // perTypePenMod: additional % pen multiplier (all formulas) |
| 268 | + // perTypeFlatPen: flat defense reduction (CUSTOM formula only) |
| 269 | + double perTypePenMod = 1.0; |
| 270 | + double perTypeFlatPen = 0.0; |
| 271 | + if (statsDamager != null && dmgAtt != null) { |
| 272 | + for (PenetrationStat penStat : ItemStats.getPenetrations()) { |
| 273 | + if (penStat.isApplicableTo(dmgAtt.getId())) { |
| 274 | + double penValue = statsDamager.getPenetration(penStat); |
| 275 | + if (penValue != 0) { |
| 276 | + if (penStat.isPercentPen()) { |
| 277 | + perTypePenMod *= Math.max(0D, 1.0 - penValue / 100.0); |
| 278 | + } else { |
| 279 | + perTypeFlatPen += penValue; |
| 280 | + } |
| 281 | + } |
| 282 | + } |
| 283 | + } |
| 284 | + } |
254 | 285 | double directType = dmgType * directMod; // Get direct value for this Damage Attribute |
255 | 286 | dmgType = Math.max(0, dmgType - directType); // Deduct this value from damage |
256 | 287 |
|
257 | 288 | if (dmgType > 0) { |
258 | | - DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; |
259 | | - if (defAtt != null && defenses.containsKey(defAtt)) { |
260 | | - double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod); |
261 | | - |
262 | | - double defCalced; |
263 | | - if (EngineCfg.LEGACY_COMBAT) { |
264 | | - defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); |
265 | | - } else { |
266 | | - defCalced = Math.max(0, |
| 289 | + if (EngineCfg.LEGACY_COMBAT) { |
| 290 | + // Legacy: 1:1, highest priority defense only |
| 291 | + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; |
| 292 | + if (defAtt != null && defenses.containsKey(defAtt)) { |
| 293 | + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); |
| 294 | + // Apply defense buff % from victim's equipment |
| 295 | + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { |
| 296 | + if (dBuff.isApplicableTo(defAtt.getId())) { |
| 297 | + double buffPct = statsVictim.getDynamicBuff(dBuff); |
| 298 | + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); |
| 299 | + } |
| 300 | + } |
| 301 | + double defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); |
| 302 | + meta.setDefendedDamage(defAtt, dmgType - defCalced); |
| 303 | + dmgType = defCalced; |
| 304 | + } |
| 305 | + } else if ("CUSTOM".equals(EngineCfg.DEFENSE_FORMULA_MODE)) { |
| 306 | + // Custom: collect ALL matching defenses (group sum + individual placeholders) |
| 307 | + double totalDef = 0; |
| 308 | + Map<String, Double> individualDefs = new HashMap<>(); |
| 309 | + for (DefenseAttribute defAtt : ItemStats.getDefenses()) { |
| 310 | + if (dmgAtt != null && defAtt.isBlockable(dmgAtt) && defenses.containsKey(defAtt)) { |
| 311 | + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); |
| 312 | + totalDef += def; |
| 313 | + individualDefs.put(defAtt.getId(), def); |
| 314 | + } |
| 315 | + } |
| 316 | + // Apply defense buff % from victim's equipment (on summed total) |
| 317 | + if (totalDef > 0 && dmgAtt != null) { |
| 318 | + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { |
| 319 | + if (dBuff.isApplicableTo(dmgAtt.getId())) { |
| 320 | + double buffPct = statsVictim.getDynamicBuff(dBuff); |
| 321 | + if (buffPct != 0) totalDef *= (1.0 + buffPct / 100.0); |
| 322 | + } |
| 323 | + } |
| 324 | + } |
| 325 | + // Apply flat penetration to total defense (CUSTOM formula only) |
| 326 | + if (perTypeFlatPen > 0) { |
| 327 | + totalDef = Math.max(0, totalDef - perTypeFlatPen); |
| 328 | + } |
| 329 | + if (totalDef > 0) { |
| 330 | + double defCalced = Math.max(0, evaluateDefenseFormula( |
| 331 | + EngineCfg.CUSTOM_DEFENSE_FORMULA, dmgType, totalDef, toughness, individualDefs)); |
| 332 | + DefenseAttribute primaryDef = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; |
| 333 | + if (primaryDef != null) { |
| 334 | + meta.setDefendedDamage(primaryDef, dmgType - defCalced); |
| 335 | + } |
| 336 | + dmgType = defCalced; |
| 337 | + } |
| 338 | + } else { |
| 339 | + // Factor: 1:1, highest priority defense only (minecraft formula) |
| 340 | + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; |
| 341 | + if (defAtt != null && defenses.containsKey(defAtt)) { |
| 342 | + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); |
| 343 | + // Apply defense buff % from victim's equipment |
| 344 | + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { |
| 345 | + if (dBuff.isApplicableTo(defAtt.getId())) { |
| 346 | + double buffPct = statsVictim.getDynamicBuff(dBuff); |
| 347 | + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); |
| 348 | + } |
| 349 | + } |
| 350 | + double defCalced = Math.max(0, |
267 | 351 | dmgType * (1 - Math.max(def / 5, def - 4 * dmgType / Math.max(1, toughness + 8)) |
268 | 352 | * defAtt.getProtectionFactor() * 0.05)); |
| 353 | + meta.setDefendedDamage(defAtt, dmgType - defCalced); |
| 354 | + dmgType = defCalced; |
269 | 355 | } |
270 | | - meta.setDefendedDamage(defAtt, dmgType - defCalced); |
271 | | - dmgType = defCalced; |
272 | 356 | } |
273 | 357 | } |
274 | 358 | //Should we reactivate direct damage, remove directType here and deal the damage straight. |
@@ -569,4 +653,18 @@ public void onDamage(DivinityDamageEvent.BeforeScale event) { |
569 | 653 | } |
570 | 654 | return success[0]; |
571 | 655 | } |
| 656 | + |
| 657 | + private static double evaluateDefenseFormula(String formula, double damage, double defense, |
| 658 | + double toughness, Map<String, Double> individualDefs) { |
| 659 | + String expr = formula |
| 660 | + .replace("damage", String.valueOf(damage)) |
| 661 | + .replace("toughness", String.valueOf(toughness)); |
| 662 | + // Replace individual defense placeholders BEFORE the sum placeholder |
| 663 | + // because "defense" is a prefix of "defense_<id>" |
| 664 | + for (Map.Entry<String, Double> entry : individualDefs.entrySet()) { |
| 665 | + expr = expr.replace("defense_" + entry.getKey(), String.valueOf(entry.getValue())); |
| 666 | + } |
| 667 | + expr = expr.replace("defense", String.valueOf(defense)); |
| 668 | + return Evaluator.eval(expr, 1); |
| 669 | + } |
572 | 670 | } |
0 commit comments