From b211dcb641a990fbe52615ea03d867f9bdbe99b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 05:38:43 +0000 Subject: [PATCH] Integrate Fabled skill-API stat scaling and attribute sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FabledHook now: - Syncs MAX_MANA (Divinity item stat) into Fabled's mana pool via PlayerMaxManaChangeEvent. - Pushes/clears Fabled PlayerAttributeModifiers derived from Divinity item attributes on equip change and player quit. - Exposes applyStatScale(player, statId, value), which lets Fabled's attributes.yml rescale any Divinity stat by name — wired into every EntityStats stat accessor (getDamageByType, getDefenseByType, getItemStat, getPenetration, getDynamicBuff) added across the preceding PRs in this stack. - Adds DivinityDamageEvent.Start/Dodge handlers that read a "fabled_dmg_flags" metadata bitmask off the damager to let Fabled skills suppress crit/skill-crit, force a block, zero bleed/vampirism, or force-cancel (dodge) on a specific hit. ItemAbilityHandler now clears a player's Fabled attribute modifiers on quit (previously only cleared on death). --- .../divinity/hooks/external/FabledHook.java | 120 +++++++++++++++++- .../divinity/modules/ModuleCache.java | 5 + .../itemgenerator/ItemAbilityHandler.java | 8 ++ .../divinity/stats/EntityStats.java | 32 +++++ 4 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java b/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java index 901c7cad..705e77f6 100644 --- a/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java +++ b/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java @@ -4,6 +4,7 @@ import org.bukkit.Material; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.entity.EntityDamageByEntityEvent; @@ -16,6 +17,7 @@ import studio.magemonkey.codex.hooks.NHook; import studio.magemonkey.codex.util.StringUT; import studio.magemonkey.divinity.Divinity; +import studio.magemonkey.divinity.api.event.DivinityDamageEvent; import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.hooks.HookClass; import studio.magemonkey.divinity.hooks.HookLevel; @@ -28,9 +30,12 @@ import studio.magemonkey.fabled.Fabled; import studio.magemonkey.fabled.api.DefaultCombatProtection; import studio.magemonkey.fabled.api.enums.ExpSource; +import studio.magemonkey.fabled.api.enums.Operation; import studio.magemonkey.fabled.api.event.DynamicTriggerEvent; import studio.magemonkey.fabled.api.event.PlayerManaGainEvent; +import studio.magemonkey.fabled.api.event.PlayerMaxManaChangeEvent; import studio.magemonkey.fabled.api.event.SkillDamageEvent; +import studio.magemonkey.fabled.api.player.PlayerAttributeModifier; import studio.magemonkey.fabled.api.player.PlayerData; import studio.magemonkey.fabled.api.player.PlayerSkill; import studio.magemonkey.fabled.api.skills.Skill; @@ -118,6 +123,17 @@ public void onRegen(PlayerManaGainEvent e) { } } + @EventHandler + public void onMaxManaChange(PlayerMaxManaChangeEvent e) { + Player player = e.getPlayerData().getPlayer(); + if (player == null) return; + + double bonus = EntityStats.get(player).getItemStat(TypedStat.Type.MAX_MANA, false); + if (bonus != 0) { + e.setMaxMana(e.getMaxMana() + bonus); + } + } + @Override public void takeMana(@NotNull Player player, double amount, boolean ofMax) { PlayerData data = Fabled.getData(player); @@ -133,7 +149,7 @@ public void takeMana(@NotNull Player player, double amount, boolean ofMax) { data.setMana(Math.max(0, cur - amount)); } - private final List divinityIgnored = new ArrayList<>(); + private final Set divinityIgnored = new HashSet<>(); @EventHandler(priority = EventPriority.MONITOR) public void skillDamage(SkillDamageEvent event) { @@ -145,10 +161,38 @@ public void skillDamage(SkillDamageEvent event) { @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void damage(EntityDamageByEntityEvent event) { - if (!(event.getDamager() instanceof LivingEntity)) - return; + UUID damagerUuid = null; + if (event.getDamager() instanceof LivingEntity) { + damagerUuid = ((LivingEntity) event.getDamager()).getUniqueId(); + } else if (event.getDamager() instanceof Projectile) { + Projectile proj = (Projectile) event.getDamager(); + if (proj.getShooter() instanceof LivingEntity) + damagerUuid = ((LivingEntity) proj.getShooter()).getUniqueId(); + } + if (damagerUuid != null) + divinityIgnored.remove(damagerUuid); + } - divinityIgnored.remove(event.getDamager().getUniqueId()); + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onDivinityDamageStart(DivinityDamageEvent.Start e) { + LivingEntity damager = e.getDamager(); + if (damager == null || !damager.hasMetadata("fabled_dmg_flags")) return; + + int flags = damager.getMetadata("fabled_dmg_flags").get(0).asInt(); + if ((flags & 0x01) != 0) e.getDamageMeta().setIgnoreCrit(true); + if ((flags & 0x04) != 0) e.getDamageMeta().setBlockModifier(1.0); + if ((flags & 0x08) != 0) e.getDamagerItemStatsMap().put(TypedStat.Type.BLEED_RATE, 0.0); + if ((flags & 0x10) != 0) e.getDamagerItemStatsMap().put(TypedStat.Type.VAMPIRISM, 0.0); + if ((flags & 0x20) != 0) e.getDamageMeta().setIgnoreSkillCrit(true); + } + + @EventHandler + public void onDivinityDodge(DivinityDamageEvent.Dodge e) { + LivingEntity damager = e.getDamager(); + if (damager == null || !damager.hasMetadata("fabled_dmg_flags")) return; + + int flags = damager.getMetadata("fabled_dmg_flags").get(0).asInt(); + if ((flags & 0x02) != 0) e.setCancelled(true); } public void ignoreDivinity(LivingEntity player, boolean ignore) { @@ -241,6 +285,58 @@ public ItemStack getAttributeIndicator(String attributeId) { return itemStack; } + private final Map> playerAttrModifiers = new HashMap<>(); + + public void updateFabledAttributes(Player player) { + new BukkitRunnable() { + @Override + public void run() { + if (!Fabled.hasPlayerData(player)) return; + PlayerData data = Fabled.getData(player); + if (data == null) return; + + List oldUuids = playerAttrModifiers.remove(player.getUniqueId()); + if (oldUuids != null) { + for (UUID uuid : oldUuids) { + data.removeAttributeModifier(uuid, false); + } + } + + List newUuids = new ArrayList<>(); + PlayerInventory inventory = player.getInventory(); + int[] slots = {inventory.getHeldItemSlot(), 36, 37, 38, 39, 40}; + for (int slot : slots) { + ItemStack item = inventory.getItem(slot); + if (item == null) continue; + for (FabledAttribute attr : getAttributes()) { + Integer value = attr.getRaw(item); + if (value == null || value == 0) continue; + PlayerAttributeModifier modifier = new PlayerAttributeModifier( + "divinity.fabled_attr", value, Operation.ADD_NUMBER, false); + newUuids.add(modifier.getUUID()); + data.addAttributeModifier(attr.getId(), modifier, false); + } + } + + if (!newUuids.isEmpty()) { + playerAttrModifiers.put(player.getUniqueId(), newUuids); + } + + data.updatePlayerStat(player); + } + }.runTaskLater(plugin, 1L); + } + + public void clearFabledAttributes(Player player) { + List uuids = playerAttrModifiers.remove(player.getUniqueId()); + if (uuids == null || !Fabled.hasPlayerData(player)) return; + PlayerData data = Fabled.getData(player); + if (data == null) return; + for (UUID uuid : uuids) { + data.removeAttributeModifier(uuid, false); + } + } + public void updateSkills(Player player) { new BukkitRunnable() { @Override @@ -284,6 +380,22 @@ public void run() { } } }.runTaskLater(plugin, 1L); + updateFabledAttributes(player); + } + + /** + * 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 { + if (!Fabled.hasPlayerData(player)) return value; + 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) { diff --git a/src/main/java/studio/magemonkey/divinity/modules/ModuleCache.java b/src/main/java/studio/magemonkey/divinity/modules/ModuleCache.java index 768b1431..33685bc6 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/ModuleCache.java +++ b/src/main/java/studio/magemonkey/divinity/modules/ModuleCache.java @@ -245,4 +245,9 @@ public CombatLogManager getCombatLogManager() { public MoneyManager getMoneyManager() { return this.moneyManager; } + + @Nullable + public ConsumablesManager getConsumablesManager() { + return this.consumablesManager; + } } \ No newline at end of file diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemAbilityHandler.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemAbilityHandler.java index 2b5873ba..4c1f07e9 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemAbilityHandler.java +++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemAbilityHandler.java @@ -70,6 +70,14 @@ public void onPlayerJoin(PlayerJoinEvent event) { } } + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + FabledHook fabledHook = (FabledHook) this.plugin.getHook(EHook.SKILL_API); + if (fabledHook != null) { + fabledHook.clearFabledAttributes(event.getPlayer()); + } + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void playerDeath(PlayerDeathEvent event) { FabledHook fabledHook = (FabledHook) this.plugin.getHook(EHook.SKILL_API); diff --git a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java index 2783f93c..b2b091c6 100644 --- a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java +++ b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java @@ -30,6 +30,8 @@ import studio.magemonkey.divinity.config.EngineCfg; import studio.magemonkey.divinity.data.api.DivinityUser; import studio.magemonkey.divinity.data.api.UserProfile; +import studio.magemonkey.divinity.hooks.EHook; +import studio.magemonkey.divinity.hooks.external.FabledHook; import studio.magemonkey.divinity.manager.damage.DamageMeta; import studio.magemonkey.divinity.manager.effects.IEffect; import studio.magemonkey.divinity.manager.effects.IEffectType; @@ -691,6 +693,10 @@ public Map getDamageTypes(boolean safe, @Nullable ItemS double value = Rnd.getDouble(range[0], range[1]); value *= dmgAtt.getDamageModifierByBiome(bio); // Multiply by Biome value = this.getEffectBonus(dmgAtt, safe).applyAsDouble(value); + if (this.isPlayer()) { + FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + if (fHook != null) value = fHook.applyStatScale(this.player, "damage_" + dmgAtt.getId(), value); + } if (value > 0D) { map.put(dmgAtt, value); @@ -724,6 +730,10 @@ public Map getDefenseTypes(boolean safe) { double value = BonusCalculator.SIMPLE_FULL.apply(0D, bonuses); value = this.getEffectBonus(dt, safe).applyAsDouble(value); + if (this.isPlayer()) { + FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + if (fHook != null) value = fHook.applyStatScale(this.player, "defense_" + dt.getId(), value); + } if (value > 0D) { map.put(dt, value); } @@ -794,6 +804,14 @@ public double getItemStat(@NotNull SimpleStat.Type type, boolean safe, @Nullable } } + // Apply Fabled attribute/stat scaling if player and Fabled is loaded + if (this.isPlayer()) { + FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + if (fHook != null) { + value = fHook.applyStatScale(this.player, type.name().toLowerCase(), value); + } + } + return value; } @@ -808,6 +826,12 @@ public double getPenetration(@NotNull PenetrationStat pen) { if (pen.getCapacity() >= 0 && value > pen.getCapacity()) { value = pen.getCapacity(); } + if (this.isPlayer()) { + FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + if (fHook != null) { + value = fHook.applyStatScale(this.player, "penetration_" + pen.getPenId(), value); + } + } return value; } @@ -822,6 +846,14 @@ public double getDynamicBuff(@NotNull DynamicBuffStat buff) { if (buff.getCapacity() >= 0 && value > buff.getCapacity()) { value = buff.getCapacity(); } + if (this.isPlayer()) { + FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); + if (fHook != null) { + String buffKey = (buff.getBuffTarget() == DynamicBuffStat.BuffTarget.DAMAGE ? "damagebuff_" : "defensebuff_") + + buff.getBuffId(); + value = fHook.applyStatScale(this.player, buffKey, value); + } + } return value; }