Skip to content

Commit 86340fd

Browse files
committed
Lotus Changes
1 parent d6faa4a commit 86340fd

34 files changed

Lines changed: 1795 additions & 67 deletions

src/main/java/studio/magemonkey/divinity/config/Config.java

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat;
1919
import studio.magemonkey.divinity.stats.items.attributes.stats.BleedStat;
2020
import studio.magemonkey.divinity.stats.items.attributes.stats.DurabilityStat;
21+
import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat;
22+
import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat;
2123
import studio.magemonkey.divinity.stats.tiers.Tier;
2224
import studio.magemonkey.divinity.types.ItemGroup;
2325
import studio.magemonkey.divinity.types.ItemSubType;
@@ -69,6 +71,9 @@ public void setupAttributes() {
6971
this.setupDamages();
7072
this.setupDefense();
7173
this.setupStats();
74+
this.setupDamageBuffs();
75+
this.setupDefenseBuffs();
76+
this.setupPenetrations();
7277
this.setupHand();
7378
this.setupAmmo();
7479
this.setupSockets();
@@ -174,20 +179,14 @@ private void setupDefense() {
174179
private void setupStats() {
175180
JYML cfg;
176181
try {
177-
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats.yml");
182+
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/general_stats.yml");
178183
} catch (InvalidConfigurationException e) {
179184
this.plugin.error("Failed to load stats config (" + this.plugin.getName()
180-
+ "/item_stats/stats.yml): Configuration error");
185+
+ "/item_stats/stats/general_stats.yml): Configuration error");
181186
e.printStackTrace();
182187
return;
183188
}
184189

185-
cfg.addMissing("ARMOR_TOUGHNESS.enabled", true);
186-
cfg.addMissing("ARMOR_TOUGHNESS.name", "Armor Toughness");
187-
cfg.addMissing("ARMOR_TOUGHNESS.format", "&9▸ %name%: &f%value% %condition%");
188-
cfg.addMissing("ARMOR_TOUGHNESS.capacity", 100.0);
189-
cfg.save();
190-
191190
for (SimpleStat.Type statType : TypedStat.Type.values()) {
192191
String path2 = statType.name() + ".";
193192
if (!cfg.getBoolean(path2 + "enabled")) {
@@ -214,6 +213,109 @@ private void setupStats() {
214213
}
215214
}
216215

216+
private void setupDamageBuffs() {
217+
JYML cfg;
218+
try {
219+
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/damage_buffs_percent.yml");
220+
} catch (InvalidConfigurationException e) {
221+
this.plugin.error("Failed to load damage_buffs_percent config: Configuration error");
222+
e.printStackTrace();
223+
return;
224+
}
225+
226+
for (DamageAttribute dmg : ItemStats.getDamages()) {
227+
String id = dmg.getId();
228+
String path = id + ".";
229+
cfg.addMissing(path + "enabled", true);
230+
cfg.addMissing(path + "name", dmg.getName() + " Buff %");
231+
cfg.addMissing(path + "format", "&3▸ %name%: &f%value%%condition%");
232+
cfg.addMissing(path + "capacity", -1.0);
233+
cfg.addMissing(path + "hook", Collections.singletonList(id));
234+
}
235+
cfg.saveChanges();
236+
237+
for (String buffId : cfg.getSection("")) {
238+
if (!cfg.getBoolean(buffId + ".enabled")) continue;
239+
String name = StringUT.color(cfg.getString(buffId + ".name", buffId));
240+
String format = StringUT.color(cfg.getString(buffId + ".format", "&3▸ %name%: &f%value%"));
241+
double cap = cfg.getDouble(buffId + ".capacity", -1D);
242+
Set<String> hooks = new HashSet<>(cfg.getStringList(buffId + ".hook"));
243+
244+
DynamicBuffStat buff = new DynamicBuffStat(
245+
DynamicBuffStat.BuffTarget.DAMAGE, buffId, name, format, hooks, cap);
246+
ItemStats.registerDamageBuff(buff);
247+
}
248+
}
249+
250+
private void setupDefenseBuffs() {
251+
JYML cfg;
252+
try {
253+
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/defense_buffs_percent.yml");
254+
} catch (InvalidConfigurationException e) {
255+
this.plugin.error("Failed to load defense_buffs_percent config: Configuration error");
256+
e.printStackTrace();
257+
return;
258+
}
259+
260+
for (DefenseAttribute def : ItemStats.getDefenses()) {
261+
String id = def.getId();
262+
String path = id + ".";
263+
cfg.addMissing(path + "enabled", true);
264+
cfg.addMissing(path + "name", def.getName() + " Buff %");
265+
cfg.addMissing(path + "format", "&9▸ %name%: &f%value%%condition%");
266+
cfg.addMissing(path + "capacity", -1.0);
267+
cfg.addMissing(path + "hook", Collections.singletonList(id));
268+
}
269+
cfg.saveChanges();
270+
271+
for (String buffId : cfg.getSection("")) {
272+
if (!cfg.getBoolean(buffId + ".enabled")) continue;
273+
String name = StringUT.color(cfg.getString(buffId + ".name", buffId));
274+
String format = StringUT.color(cfg.getString(buffId + ".format", "&9▸ %name%: &f%value%"));
275+
double cap = cfg.getDouble(buffId + ".capacity", -1D);
276+
Set<String> hooks = new HashSet<>(cfg.getStringList(buffId + ".hook"));
277+
278+
DynamicBuffStat buff = new DynamicBuffStat(
279+
DynamicBuffStat.BuffTarget.DEFENSE, buffId, name, format, hooks, cap);
280+
ItemStats.registerDefenseBuff(buff);
281+
}
282+
}
283+
284+
private void setupPenetrations() {
285+
JYML cfg;
286+
try {
287+
cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/penetration.yml");
288+
} catch (InvalidConfigurationException e) {
289+
this.plugin.error("Failed to load penetration config: Configuration error");
290+
e.printStackTrace();
291+
return;
292+
}
293+
294+
// Auto-generate a flat-pen entry for every registered damage type (if missing)
295+
for (DamageAttribute dmg : ItemStats.getDamages()) {
296+
String id = dmg.getId() + "_pen";
297+
String path = id + ".";
298+
cfg.addMissing(path + "enabled", true);
299+
cfg.addMissing(path + "name", dmg.getName() + " Penetration");
300+
cfg.addMissing(path + "format", "&c▸ %name%: &f%value%%condition%");
301+
cfg.addMissing(path + "capacity", -1.0);
302+
cfg.addMissing(path + "percent-pen", false);
303+
cfg.addMissing(path + "hooks", Collections.singletonList(dmg.getId()));
304+
}
305+
cfg.saveChanges();
306+
307+
for (String penId : cfg.getSection("")) {
308+
if (!cfg.getBoolean(penId + ".enabled")) continue;
309+
String name = StringUT.color(cfg.getString(penId + ".name", penId));
310+
String format = StringUT.color(cfg.getString(penId + ".format", "&c▸ %name%: &f%value%"));
311+
double cap = cfg.getDouble(penId + ".capacity", -1D);
312+
boolean percentPen = cfg.getBoolean(penId + ".percent-pen", false);
313+
Set<String> hooks = new HashSet<>(cfg.getStringList(penId + ".hooks"));
314+
315+
new PenetrationStat(penId, name, format, hooks, percentPen, cap);
316+
}
317+
}
318+
217319
private void setupHand() {
218320
JYML cfg;
219321
try {

src/main/java/studio/magemonkey/divinity/config/EngineCfg.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException
5757
public static double COMBAT_SHIELD_BLOCK_BONUS_DAMAGE_MOD;
5858
public static int COMBAT_SHIELD_BLOCK_COOLDOWN;
5959
public static boolean LEGACY_COMBAT;
60+
public static String DEFENSE_FORMULA_MODE;
61+
public static String CUSTOM_DEFENSE_FORMULA;
6062
public static boolean FULL_LEGACY;
6163
public static boolean COMBAT_DISABLE_VANILLA_SWEEP;
6264
public static boolean COMBAT_REDUCE_PLAYER_HEALTH_BAR;
@@ -110,8 +112,9 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException
110112
public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_SEPAR;
111113
public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_COLOR;
112114

113-
public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN;
114-
public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN;
115+
public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN;
116+
public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN;
117+
public static boolean LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM;
115118

116119
public static String LORE_STYLE_FABLED_ATTRIBUTE_FORMAT;
117120

@@ -211,6 +214,8 @@ public void setup() {
211214

212215
path = "combat.";
213216
EngineCfg.LEGACY_COMBAT = cfg.getBoolean(path + "legacy-combat", false);
217+
EngineCfg.DEFENSE_FORMULA_MODE = cfg.getString(path + "defense-formula", "FACTOR").toUpperCase();
218+
EngineCfg.CUSTOM_DEFENSE_FORMULA = cfg.getString(path + "custom-defense-formula", "damage*(25/(25+defense))");
214219
EngineCfg.COMBAT_DISABLE_VANILLA_SWEEP = cfg.getBoolean(path + "disable-vanilla-sweep-attack");
215220
EngineCfg.COMBAT_REDUCE_PLAYER_HEALTH_BAR = cfg.getBoolean(path + "compress-player-health-bar");
216221
EngineCfg.COMBAT_FISHING_HOOK_DO_DAMAGE = cfg.getBoolean(path + "fishing-hook-do-damage");
@@ -418,9 +423,11 @@ public void setup() {
418423
path = "lore.stats.style.enchantments.";
419424
cfg.addMissing(path + "format.main", "&c▸ %name% %value%");
420425
cfg.addMissing(path + "format.max-roman", 10);
426+
cfg.addMissing(path + "roman-system", true);
421427
EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN =
422428
StringUT.color(cfg.getString(path + "format.main", "&c▸ %name% %value%"));
423429
EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN = cfg.getInt(path + "format.max-roman", 10);
430+
EngineCfg.LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM = cfg.getBoolean(path + "roman-system", true);
424431

425432
path = "lore.stats.style.fabled-attribute-format";
426433
cfg.addMissing(path, "&7%attrPre%&3%name%&7%attrPost%");

src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,20 @@ public void run() {
286286
}.runTaskLater(plugin, 1L);
287287
}
288288

289+
/**
290+
* Scales a Divinity stat value using Fabled's attribute and stat modifier system.
291+
* Fabled attributes.yml can reference Divinity stat names (lowercase type names, e.g. "critical_rate").
292+
*/
293+
public double applyStatScale(@NotNull Player player, @NotNull String statId, double value) {
294+
try {
295+
PlayerData data = Fabled.getData(player);
296+
if (data == null) return value;
297+
return data.scaleStat(statId, value);
298+
} catch (Exception ignored) {
299+
return value;
300+
}
301+
}
302+
289303
public boolean isFakeDamage(EntityDamageByEntityEvent event) {
290304
return DefaultCombatProtection.isFakeDamageEvent(event);
291305
}

src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.jetbrains.annotations.NotNull;
2424
import org.jetbrains.annotations.Nullable;
2525
import studio.magemonkey.codex.api.items.PrefixHelper;
26+
import studio.magemonkey.codex.util.eval.Evaluator;
2627
import studio.magemonkey.codex.hooks.Hooks;
2728
import studio.magemonkey.codex.manager.IListener;
2829
import studio.magemonkey.codex.registry.provider.DamageTypeProvider;
@@ -46,6 +47,8 @@
4647
import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat;
4748
import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat;
4849
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;
4952

5053
import java.util.*;
5154
import java.util.function.DoubleUnaryOperator;
@@ -251,24 +254,105 @@ public void onDamageRPGStart(@NotNull DivinityDamageEvent.Start e) {
251254
if (!e.isExempt())
252255
dmgType *= powerMod;
253256
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+
}
254285
double directType = dmgType * directMod; // Get direct value for this Damage Attribute
255286
dmgType = Math.max(0, dmgType - directType); // Deduct this value from damage
256287

257288
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,
267351
dmgType * (1 - Math.max(def / 5, def - 4 * dmgType / Math.max(1, toughness + 8))
268352
* defAtt.getProtectionFactor() * 0.05));
353+
meta.setDefendedDamage(defAtt, dmgType - defCalced);
354+
dmgType = defCalced;
269355
}
270-
meta.setDefendedDamage(defAtt, dmgType - defCalced);
271-
dmgType = defCalced;
272356
}
273357
}
274358
//Should we reactivate direct damage, remove directType here and deal the damage straight.
@@ -569,4 +653,18 @@ public void onDamage(DivinityDamageEvent.BeforeScale event) {
569653
}
570654
return success[0];
571655
}
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+
}
572670
}

0 commit comments

Comments
 (0)