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
118 changes: 110 additions & 8 deletions src/main/java/studio/magemonkey/divinity/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
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.DurabilityStat;
import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat;
import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat;
import studio.magemonkey.divinity.stats.tiers.Tier;
import studio.magemonkey.divinity.types.ItemGroup;
import studio.magemonkey.divinity.types.ItemSubType;
Expand Down Expand Up @@ -69,6 +71,9 @@ public void setupAttributes() {
this.setupDamages();
this.setupDefense();
this.setupStats();
this.setupDamageBuffs();
this.setupDefenseBuffs();
this.setupPenetrations();
this.setupHand();
this.setupAmmo();
this.setupSockets();
Expand Down Expand Up @@ -174,20 +179,14 @@ private void setupDefense() {
private void setupStats() {
JYML cfg;
try {
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats.yml");
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/general_stats.yml");
} catch (InvalidConfigurationException e) {
this.plugin.error("Failed to load stats config (" + this.plugin.getName()
+ "/item_stats/stats.yml): Configuration error");
+ "/item_stats/stats/general_stats.yml): Configuration error");
e.printStackTrace();
return;
}

cfg.addMissing("ARMOR_TOUGHNESS.enabled", true);
cfg.addMissing("ARMOR_TOUGHNESS.name", "Armor Toughness");
cfg.addMissing("ARMOR_TOUGHNESS.format", "&9▸ %name%: &f%value% %condition%");
cfg.addMissing("ARMOR_TOUGHNESS.capacity", 100.0);
cfg.save();

for (SimpleStat.Type statType : TypedStat.Type.values()) {
String path2 = statType.name() + ".";
if (!cfg.getBoolean(path2 + "enabled")) {
Expand All @@ -214,6 +213,109 @@ private void setupStats() {
}
}

private void setupDamageBuffs() {
JYML cfg;
try {
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/damage_buffs_percent.yml");
} catch (InvalidConfigurationException e) {
this.plugin.error("Failed to load damage_buffs_percent config: Configuration error");
e.printStackTrace();
return;
}

for (DamageAttribute dmg : ItemStats.getDamages()) {
String id = dmg.getId();
String path = id + ".";
cfg.addMissing(path + "enabled", true);
cfg.addMissing(path + "name", dmg.getName() + " Buff %");
cfg.addMissing(path + "format", "&3▸ %name%: &f%value%%condition%");
cfg.addMissing(path + "capacity", -1.0);
cfg.addMissing(path + "hook", Collections.singletonList(id));
}
cfg.saveChanges();

for (String buffId : cfg.getSection("")) {
if (!cfg.getBoolean(buffId + ".enabled")) continue;
String name = StringUT.color(cfg.getString(buffId + ".name", buffId));
String format = StringUT.color(cfg.getString(buffId + ".format", "&3▸ %name%: &f%value%"));
double cap = cfg.getDouble(buffId + ".capacity", -1D);
Set<String> hooks = new HashSet<>(cfg.getStringList(buffId + ".hook"));

DynamicBuffStat buff = new DynamicBuffStat(
DynamicBuffStat.BuffTarget.DAMAGE, buffId, name, format, hooks, cap);
ItemStats.registerDamageBuff(buff);
}
}

private void setupDefenseBuffs() {
JYML cfg;
try {
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/defense_buffs_percent.yml");
} catch (InvalidConfigurationException e) {
this.plugin.error("Failed to load defense_buffs_percent config: Configuration error");
e.printStackTrace();
return;
}

for (DefenseAttribute def : ItemStats.getDefenses()) {
String id = def.getId();
String path = id + ".";
cfg.addMissing(path + "enabled", true);
cfg.addMissing(path + "name", def.getName() + " Buff %");
cfg.addMissing(path + "format", "&9▸ %name%: &f%value%%condition%");
cfg.addMissing(path + "capacity", -1.0);
cfg.addMissing(path + "hook", Collections.singletonList(id));
}
cfg.saveChanges();

for (String buffId : cfg.getSection("")) {
if (!cfg.getBoolean(buffId + ".enabled")) continue;
String name = StringUT.color(cfg.getString(buffId + ".name", buffId));
String format = StringUT.color(cfg.getString(buffId + ".format", "&9▸ %name%: &f%value%"));
double cap = cfg.getDouble(buffId + ".capacity", -1D);
Set<String> hooks = new HashSet<>(cfg.getStringList(buffId + ".hook"));

DynamicBuffStat buff = new DynamicBuffStat(
DynamicBuffStat.BuffTarget.DEFENSE, buffId, name, format, hooks, cap);
ItemStats.registerDefenseBuff(buff);
}
}

private void setupPenetrations() {
JYML cfg;
try {
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/penetration.yml");
} catch (InvalidConfigurationException e) {
this.plugin.error("Failed to load penetration config: Configuration error");
e.printStackTrace();
return;
}

// Auto-generate a flat-pen entry for every registered damage type (if missing)
for (DamageAttribute dmg : ItemStats.getDamages()) {
String id = dmg.getId() + "_pen";
String path = id + ".";
cfg.addMissing(path + "enabled", true);
cfg.addMissing(path + "name", dmg.getName() + " Penetration");
cfg.addMissing(path + "format", "&c▸ %name%: &f%value%%condition%");
cfg.addMissing(path + "capacity", -1.0);
cfg.addMissing(path + "percent-pen", false);
cfg.addMissing(path + "hooks", Collections.singletonList(dmg.getId()));
}
cfg.saveChanges();

for (String penId : cfg.getSection("")) {
if (!cfg.getBoolean(penId + ".enabled")) continue;
String name = StringUT.color(cfg.getString(penId + ".name", penId));
String format = StringUT.color(cfg.getString(penId + ".format", "&c▸ %name%: &f%value%"));
double cap = cfg.getDouble(penId + ".capacity", -1D);
boolean percentPen = cfg.getBoolean(penId + ".percent-pen", false);
Set<String> hooks = new HashSet<>(cfg.getStringList(penId + ".hooks"));

new PenetrationStat(penId, name, format, hooks, percentPen, cap);
}
}

private void setupHand() {
JYML cfg;
try {
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/studio/magemonkey/divinity/config/EngineCfg.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ 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 FULL_LEGACY;
public static boolean COMBAT_DISABLE_VANILLA_SWEEP;
public static boolean COMBAT_REDUCE_PLAYER_HEALTH_BAR;
Expand Down Expand Up @@ -110,8 +112,9 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException
public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_SEPAR;
public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_COLOR;

public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN;
public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN;
public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN;
public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN;
public static boolean LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM;

public static String LORE_STYLE_FABLED_ATTRIBUTE_FORMAT;

Expand Down Expand Up @@ -211,6 +214,8 @@ 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_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 Expand Up @@ -418,9 +423,11 @@ public void setup() {
path = "lore.stats.style.enchantments.";
cfg.addMissing(path + "format.main", "&c▸ %name% %value%");
cfg.addMissing(path + "format.max-roman", 10);
cfg.addMissing(path + "roman-system", true);
EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN =
StringUT.color(cfg.getString(path + "format.main", "&c▸ %name% %value%"));
EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN = cfg.getInt(path + "format.max-roman", 10);
EngineCfg.LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM = cfg.getBoolean(path + "roman-system", true);

path = "lore.stats.style.fabled-attribute-format";
cfg.addMissing(path, "&7%attrPre%&3%name%&7%attrPost%");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ public void run() {
}.runTaskLater(plugin, 1L);
}

/**
* Scales a Divinity stat value using Fabled's attribute and stat modifier system.
* Fabled attributes.yml can reference Divinity stat names (lowercase type names, e.g. "critical_rate").
*/
public double applyStatScale(@NotNull Player player, @NotNull String statId, double value) {
try {
PlayerData data = Fabled.getData(player);
if (data == null) return value;
return data.scaleStat(statId, value);
} catch (Exception ignored) {
return value;
}
}

public boolean isFakeDamage(EntityDamageByEntityEvent event) {
return DefaultCombatProtection.isFakeDamageEvent(event);
}
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,105 @@ 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)
if (perTypeFlatPen > 0) {
totalDef = Math.max(0, totalDef - perTypeFlatPen);
}
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;
}
} 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 Down Expand Up @@ -569,4 +653,18 @@ public void onDamage(DivinityDamageEvent.BeforeScale event) {
}
return success[0];
}

private 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);
}
}
Loading