From 67461082a5f42d3446c92cbebced229380a1d003 Mon Sep 17 00:00:00 2001 From: yhy <1763917516@qq.com> Date: Sat, 11 Jul 2026 22:13:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 16 + pom.xml | 43 ++ .../java/com/io/yaohun/myitems/MyItems.java | 176 +++++++ .../io/yaohun/myitems/api/AttributeData.java | 46 ++ .../io/yaohun/myitems/api/AttributeType.java | 53 ++ .../io/yaohun/myitems/api/DamageResult.java | 67 +++ .../myitems/api/EntityAttributeAPI.java | 116 +++++ .../yaohun/myitems/api/EquipmentSlotType.java | 11 + .../com/io/yaohun/myitems/api/TierType.java | 33 ++ .../myitems/builder/ItemTemplateBuilder.java | 84 ++++ .../yaohun/myitems/builder/LoreBuilder.java | 172 +++++++ .../yaohun/myitems/commands/AttCommand.java | 189 ++++++++ .../myitems/commands/MyItemCommand.java | 248 ++++++++++ .../myitems/commands/RepairCommand.java | 123 +++++ .../com/io/yaohun/myitems/config/Config.java | 15 + .../io/yaohun/myitems/data/CEngineData.java | 50 ++ .../io/yaohun/myitems/data/ItemTemplate.java | 146 ++++++ .../io/yaohun/myitems/data/SuitTemplate.java | 50 ++ .../yaohun/myitems/engine/DamageEngine.java | 127 +++++ .../com/io/yaohun/myitems/gui/PanelGui.java | 136 ++++++ .../myitems/listener/DamageListener.java | 126 +++++ .../myitems/listener/DurabilityListener.java | 153 ++++++ .../listener/EntityCleanupListener.java | 33 ++ .../myitems/listener/PlayerEquipListener.java | 153 ++++++ .../myitems/listener/PlayerJoinListener.java | 52 ++ .../yaohun/myitems/listener/SuitListener.java | 143 ++++++ .../listener/ToolDurabilityListener.java | 77 +++ .../myitems/manage/AttributeManager.java | 269 ++++++++++ .../yaohun/myitems/manage/CacheManager.java | 28 ++ .../manage/EntityAttributeStorage.java | 78 +++ .../manage/EquipmentAttributeManager.java | 4 + .../io/yaohun/myitems/manage/ItemManager.java | 192 ++++++++ .../io/yaohun/myitems/manage/SuitManager.java | 269 ++++++++++ .../myitems/model/PlayerAttributeProfile.java | 45 ++ .../myitems/util/AttributeFormatUtil.java | 54 +++ .../io/yaohun/myitems/util/AttributeKeys.java | 54 +++ .../com/io/yaohun/myitems/util/DemonAPI.java | 58 +++ .../myitems/util/EquipmentCheckUtil.java | 46 ++ .../myitems/util/EquipmentTypeUtil.java | 80 +++ .../io/yaohun/myitems/util/HealthUtil.java | 33 ++ .../myitems/util/ItemAttributeEditor.java | 241 +++++++++ .../myitems/util/ItemAttributeUtil.java | 298 ++++++++++++ .../io/yaohun/myitems/util/MessageUtil.java | 71 +++ .../io/yaohun/myitems/util/RandomBuilder.java | 83 ++++ .../io/yaohun/myitems/util/RandomUtil.java | 74 +++ .../io/yaohun/myitems/util/RepairUtil.java | 37 ++ .../com/io/yaohun/myitems/util/SpeedUtil.java | 37 ++ .../resources/Item_Generator/稀有工具箱.yml | 15 + src/main/resources/config.yml | 7 + src/main/resources/lang.yml | 40 ++ src/main/resources/plugin.yml | 12 + 使用案例.md | 347 +++++++++++++ 插件投入使用测试流程.md | 458 ++++++++++++++++++ 53 files changed, 5568 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/io/yaohun/myitems/MyItems.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/AttributeData.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/AttributeType.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/DamageResult.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/EntityAttributeAPI.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/EquipmentSlotType.java create mode 100644 src/main/java/com/io/yaohun/myitems/api/TierType.java create mode 100644 src/main/java/com/io/yaohun/myitems/builder/ItemTemplateBuilder.java create mode 100644 src/main/java/com/io/yaohun/myitems/builder/LoreBuilder.java create mode 100644 src/main/java/com/io/yaohun/myitems/commands/AttCommand.java create mode 100644 src/main/java/com/io/yaohun/myitems/commands/MyItemCommand.java create mode 100644 src/main/java/com/io/yaohun/myitems/commands/RepairCommand.java create mode 100644 src/main/java/com/io/yaohun/myitems/config/Config.java create mode 100644 src/main/java/com/io/yaohun/myitems/data/CEngineData.java create mode 100644 src/main/java/com/io/yaohun/myitems/data/ItemTemplate.java create mode 100644 src/main/java/com/io/yaohun/myitems/data/SuitTemplate.java create mode 100644 src/main/java/com/io/yaohun/myitems/engine/DamageEngine.java create mode 100644 src/main/java/com/io/yaohun/myitems/gui/PanelGui.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/DamageListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/DurabilityListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/EntityCleanupListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/PlayerEquipListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/PlayerJoinListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/SuitListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/listener/ToolDurabilityListener.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/AttributeManager.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/CacheManager.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/EntityAttributeStorage.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/EquipmentAttributeManager.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/ItemManager.java create mode 100644 src/main/java/com/io/yaohun/myitems/manage/SuitManager.java create mode 100644 src/main/java/com/io/yaohun/myitems/model/PlayerAttributeProfile.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/AttributeFormatUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/AttributeKeys.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/DemonAPI.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/EquipmentCheckUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/EquipmentTypeUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/HealthUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/ItemAttributeEditor.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/ItemAttributeUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/MessageUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/RandomBuilder.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/RandomUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/RepairUtil.java create mode 100644 src/main/java/com/io/yaohun/myitems/util/SpeedUtil.java create mode 100644 src/main/resources/Item_Generator/稀有工具箱.yml create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/lang.yml create mode 100644 src/main/resources/plugin.yml create mode 100644 使用案例.md create mode 100644 插件投入使用测试流程.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d0414d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +target/ +out/ +lib/ +libs/ +.vscode/ +.codex/ +.idea/ +*.iml +*.class +*.log +logs/ +*.db +*.sqlite +*.sqlite3 +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..87414b5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.example + AuMyItems + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + public-survive + https://repo.aurora-pixels.com/repository/public-survive/ + + + + + + io.papermc.paper + paper-api + 1.21.11-R0.1-SNAPSHOT + provided + + + net.momirealms.craftengine.bukkit.plugin + CraftEngine + 26.5.1 + provided + + + + diff --git a/src/main/java/com/io/yaohun/myitems/MyItems.java b/src/main/java/com/io/yaohun/myitems/MyItems.java new file mode 100644 index 0000000..09ec538 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/MyItems.java @@ -0,0 +1,176 @@ +package com.io.yaohun.myitems; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EntityAttributeAPI; +import com.io.yaohun.myitems.builder.ItemTemplateBuilder; +import com.io.yaohun.myitems.commands.AttCommand; +import com.io.yaohun.myitems.commands.MyItemCommand; +import com.io.yaohun.myitems.commands.RepairCommand; +import com.io.yaohun.myitems.config.Config; +import com.io.yaohun.myitems.engine.DamageEngine; +import com.io.yaohun.myitems.gui.PanelGui; +import com.io.yaohun.myitems.listener.*; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.manage.CacheManager; +import com.io.yaohun.myitems.manage.EntityAttributeStorage; +import com.io.yaohun.myitems.manage.ItemManager; +import com.io.yaohun.myitems.manage.SuitManager; +import com.io.yaohun.myitems.util.*; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.IronGolem; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; + +public class MyItems extends JavaPlugin { + + private static MyItems instance; + private CacheManager cacheManager; + private DamageEngine damageEngine; + private AttributeManager attributeManager; + private EntityAttributeStorage entityAttributeStorage; + private SuitManager suitManager; + + @Override + public void onEnable() { + instance = this; + Config.reloadConfig(this); + MessageUtil.init(this); + this.cacheManager = new CacheManager(); + this.entityAttributeStorage = new EntityAttributeStorage(); + this.suitManager = new SuitManager(); + this.attributeManager = new AttributeManager(cacheManager, suitManager); + this.damageEngine = new DamageEngine(attributeManager); + + getServer().getPluginManager().registerEvents(new PlayerJoinListener(attributeManager,cacheManager), this); + getServer().getPluginManager().registerEvents(new PlayerEquipListener(attributeManager), this); + getServer().getPluginManager().registerEvents(new DamageListener(damageEngine), this); + getServer().getPluginManager().registerEvents(new DurabilityListener(attributeManager), this); + getServer().getPluginManager().registerEvents(new ToolDurabilityListener(attributeManager), this); + getServer().getPluginManager().registerEvents(new SuitListener(suitManager, attributeManager), this); + getServer().getPluginManager().registerEvents(new EntityCleanupListener(entityAttributeStorage), this); + + getCommand("att").setExecutor(new AttCommand()); + getCommand("repair").setExecutor(new RepairCommand()); + getCommand("myitems").setExecutor(new MyItemCommand()); + + Bukkit.getScheduler().runTask(this, new Runnable() { + @Override + public void run() { + ItemManager.reloadItemManager(MyItems.inst()); + suitManager.reloadSuitManager(MyItems.inst()); + for (Player player : Bukkit.getOnlinePlayers()){ + suitManager.refreshPlayerSuitState(player, false); + attributeManager.refresh(player); + } + } + }); + } + + @Override + public void onDisable() { + if(cacheManager != null){ + cacheManager.clear(); + } + } + + + public static MyItems inst() { + return instance; + } + + public CacheManager getCacheManager() { + return cacheManager; + } + + public AttributeManager getAttributeManager() { + return attributeManager; + } + + public DamageEngine getDamageEngine() { + return damageEngine; + } + + public EntityAttributeStorage getEntityAttributeStorage() { + return entityAttributeStorage; + } + + public SuitManager getSuitManager() { + return suitManager; + } + + public ItemStack gethelmet() { + ItemStack helmet = new ItemStack(Material.DIAMOND_HELMET); + ItemMeta meta = helmet.getItemMeta(); + meta.setDisplayName("§b战神头盔"); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + helmet.setItemMeta(meta); + + ItemAttributeEditor.setAttribute(helmet, AttributeType.PHYSICAL_DEFENSE, 30); + ItemAttributeEditor.setAttribute(helmet, AttributeType.MAX_HEALTH_BONUS, 40); + ItemAttributeEditor.setAttribute(helmet, AttributeType.DODGE_CHANCE, 8); + ItemAttributeEditor.setDurability(helmet, 80, 80); + return helmet; + } + + public ItemStack getsword() { + ItemStack sword = new ItemStack(Material.DIAMOND_SWORD); + ItemMeta meta = sword.getItemMeta(); + meta.setDisplayName("§6测试神剑"); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + sword.setItemMeta(meta); + ItemAttributeEditor.setAttribute(sword, AttributeType.PHYSICAL_ATTACK, 100); + ItemAttributeEditor.setAttribute(sword, AttributeType.CRIT_CHANCE, 25); + ItemAttributeEditor.setAttribute(sword, AttributeType.CRIT_DAMAGE, 50); + ItemAttributeEditor.setAttribute(sword, AttributeType.HIT_CHANCE, 10); + ItemAttributeEditor.setDurability(sword, 20, 120); + return sword; + } + + public ItemStack getPickAxe() { + ItemStack stack = new ItemStack(Material.IRON_PICKAXE); + int max = 1024; + int current = RandomUtil.getRandomInt(1,20); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§7[§e超稀有§7]§r铁镐"); + List lore = new ArrayList<>(); + + String color; + double percent = current * 1.0 / max; + if(current <= 0){ + color = "§c"; + }else if(percent > 0.5){ + color = "§a"; + }else if(percent > 0.2){ + color = "§e"; + }else { + color = "§c"; + } + AttributeType type = AttributeType.EQUIPMENT_DURABILITY; + String prefix = MessageUtil.getMessage(type.name()); + String newLore = prefix+" "+color+ AttributeFormatUtil.format(type, current)+"§7/§a"+ max; + + lore.add(newLore); + lore.add("§a✔§f从稀有工具箱I获得"); + meta.setLore(lore); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + stack.setItemMeta(meta); + ItemAttributeEditor.setDurability(stack, current, max); + return stack; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/api/AttributeData.java b/src/main/java/com/io/yaohun/myitems/api/AttributeData.java new file mode 100644 index 0000000..1bb5dfe --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/AttributeData.java @@ -0,0 +1,46 @@ +package com.io.yaohun.myitems.api; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +public class AttributeData { + + private final EnumMap values = new EnumMap<>(AttributeType.class); + + public void add(AttributeType type, double value) { + values.put(type, get(type) + value); + } + + public void set(AttributeType type, double value) { + values.put(type, value); + } + + public double get(AttributeType type) { + return values.getOrDefault(type, type.getDefaultValue()); + } + + public boolean has(AttributeType type) { + return values.containsKey(type); + } + + public void merge(AttributeData other) { + if (other == null) return; + for (Map.Entry entry : other.getAll().entrySet()) { + add(entry.getKey(), entry.getValue()); + } + } + + public void clear() { + values.clear(); + } + + public Map getAll() { + return Collections.unmodifiableMap(values); + } + + @Override + public String toString() { + return "AttributeData{" + values + '}'; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/api/AttributeType.java b/src/main/java/com/io/yaohun/myitems/api/AttributeType.java new file mode 100644 index 0000000..3420298 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/AttributeType.java @@ -0,0 +1,53 @@ +package com.io.yaohun.myitems.api; + +public enum AttributeType { + + PHYSICAL_ATTACK("物理攻击", 0), // 攻击所有实体时造成的伤害 + PHYSICAL_DEFENSE("物理防御", 0), // 物理防御 与 物理攻击 相互抵消 + + PVE_ATTACK("PVE攻击", 0), // 在攻击非Player实体时额外造成的伤害 + PVE_ATTACK_ADDITION("PVE攻击百分比", 0), // 增加PVE攻击百分比 + PVE_DEFENSE("PVE防御", 0), // PVE防御 与 PVE攻击 相互抵消 + + MAX_HEALTH_BONUS("生命增加", 0), // 提升玩家最大生命值 + + CRIT_CHANCE("暴击概率", 0), // 触发暴击伤害的概率 + CRIT_DAMAGE("暴击伤害", 0), // 暴击伤害默认翻倍~ 而这个数值是提高倍率的 + + HIT_CHANCE("命中概率", 0), // 与闪避几率形成 1:1 相互抵消 + DODGE_CHANCE("闪避概率", 0), // 触发闪避后免疫本次伤害 + + MOVE_SPEED("移动加成", 0), // 增加移动速度 + + EQUIPMENT_DURABILITY("耐久", 0), // 耐久没有后属性全部失效 + + /** + * 套装效果属性 + * */ + SUIT_SKILL_DIAOLING("凋零锐锋",0), + SUIT_SKILL_SHOUHU("守护壁垒",0), + SUIT_SKILL_CANGMU("仓木生息",0), + SUIT_SKILL_BINGSHUANG("冰霜韧战",0), + /** + * 2026年7月新增属性 + * */ + BLOCK_CHANCE("格挡概率",0), // 触发格挡后减少50%的受到伤害,并返还10%的伤害给对方 + VAMPIRE_CHANCE("吸血概率",0), // 触发吸血后本次造成伤害的3%转换为生命值 + PENETRATION("防御穿透", 0); // 触发护甲穿透后无视防御数值 + + private final String displayName; + private final double defaultValue; + + AttributeType(String displayName, double defaultValue) { + this.displayName = displayName; + this.defaultValue = defaultValue; + } + + public String getDisplayName() { + return displayName; + } + + public double getDefaultValue() { + return defaultValue; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/api/DamageResult.java b/src/main/java/com/io/yaohun/myitems/api/DamageResult.java new file mode 100644 index 0000000..c6e1275 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/DamageResult.java @@ -0,0 +1,67 @@ +package com.io.yaohun.myitems.api; + +public class DamageResult { + + private final boolean miss; + private final boolean crit; + private final boolean penetration; + private final boolean block; + private final boolean vampire; + private final double damage; + private final double reflectDamage; + private final double healAmount; + + public DamageResult(boolean miss, boolean crit, double damage) { + this(miss, crit, false, false, false, damage, 0.0D, 0.0D); + } + + public DamageResult(boolean miss, + boolean crit, + boolean penetration, + boolean block, + boolean vampire, + double damage, + double reflectDamage, + double healAmount) { + this.miss = miss; + this.crit = crit; + this.penetration = penetration; + this.block = block; + this.vampire = vampire; + this.damage = damage; + this.reflectDamage = reflectDamage; + this.healAmount = healAmount; + } + + public boolean isMiss() { + return miss; + } + + public boolean isCrit() { + return crit; + } + + public boolean isPenetration() { + return penetration; + } + + public boolean isBlock() { + return block; + } + + public boolean isVampire() { + return vampire; + } + + public double getDamage() { + return damage; + } + + public double getReflectDamage() { + return reflectDamage; + } + + public double getHealAmount() { + return healAmount; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/api/EntityAttributeAPI.java b/src/main/java/com/io/yaohun/myitems/api/EntityAttributeAPI.java new file mode 100644 index 0000000..b08d3d5 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/EntityAttributeAPI.java @@ -0,0 +1,116 @@ +package com.io.yaohun.myitems.api; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.manage.EntityAttributeStorage; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; + +import java.util.Map; + +public final class EntityAttributeAPI { + + public static void addEntityAttribute(Entity entity, String source, Map attributeMap) { + if (!(entity instanceof LivingEntity livingEntity)) { + return; + } + if (source == null || source.isEmpty() || attributeMap == null || attributeMap.isEmpty()) { + return; + } + AttributeData data = new AttributeData(); + + for (Map.Entry entry : attributeMap.entrySet()) { + AttributeType type = matchType(entry.getKey()); + if (type == null) { + continue; + } + + data.add(type, entry.getValue()); + } + EntityAttributeStorage storage = MyItems.inst().getEntityAttributeStorage(); + storage.addAttributes(livingEntity.getUniqueId(), source, data); + } + + public static void addEntityAttribute(Entity entity, String source, AttributeType type, double value) { + if (!(entity instanceof LivingEntity livingEntity) || type == null) { + return; + } + + AttributeData data = new AttributeData(); + data.add(type, value); + + MyItems.inst() + .getEntityAttributeStorage() + .addAttributes(livingEntity.getUniqueId(), source, data); + } + + public static void removeEntityAttribute(Entity entity, String source) { + if (!(entity instanceof LivingEntity livingEntity)) { + return; + } + MyItems.inst() + .getEntityAttributeStorage() + .removeAttributes(livingEntity.getUniqueId(), source); + } + + public static AttributeData getFinalEntityAttribute(Entity entity) { + if (!(entity instanceof LivingEntity livingEntity)) { + return new AttributeData(); + } + + return MyItems.inst() + .getEntityAttributeStorage() + .getFinalAttributes(livingEntity.getUniqueId()); + } + + public static double getEntityAttribute(Entity entity, AttributeType type) { + if (!(entity instanceof LivingEntity livingEntity)) { + return 0.0D; + } + + return MyItems.inst() + .getEntityAttributeStorage() + .getAttribute(livingEntity.getUniqueId(), type); + } + + private static AttributeType matchType(String key) { + if (key == null) { + return null; + } + + switch (key.toLowerCase()) { + case "physical_attack": + return AttributeType.PHYSICAL_ATTACK; + case "physical_defense": + return AttributeType.PHYSICAL_DEFENSE; + case "pve_attack": + return AttributeType.PVE_ATTACK; + case "pve_defense": + return AttributeType.PVE_DEFENSE; + case "max_health_bonus": + case "health": + case "health_bonus": + return AttributeType.MAX_HEALTH_BONUS; + case "crit_chance": + return AttributeType.CRIT_CHANCE; + case "crit_damage": + return AttributeType.CRIT_DAMAGE; + case "hit_chance": + return AttributeType.HIT_CHANCE; + case "dodge_chance": + return AttributeType.DODGE_CHANCE; + case "block_chance": + case "block": + return AttributeType.BLOCK_CHANCE; + case "vampire_chance": + case "vampire": + return AttributeType.VAMPIRE_CHANCE; + case "penetration": + return AttributeType.PENETRATION; + case "move_speed": + return AttributeType.MOVE_SPEED; + default: + return null; + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/api/EquipmentSlotType.java b/src/main/java/com/io/yaohun/myitems/api/EquipmentSlotType.java new file mode 100644 index 0000000..a303e60 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/EquipmentSlotType.java @@ -0,0 +1,11 @@ +package com.io.yaohun.myitems.api; + +public enum EquipmentSlotType { + MAIN_HAND, + OFF_HAND, + HELMET, + CHESTPLATE, + LEGGINGS, + BOOTS, + UNIVERSAL +} diff --git a/src/main/java/com/io/yaohun/myitems/api/TierType.java b/src/main/java/com/io/yaohun/myitems/api/TierType.java new file mode 100644 index 0000000..8de07ab --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/api/TierType.java @@ -0,0 +1,33 @@ +package com.io.yaohun.myitems.api; + +import com.io.yaohun.myitems.config.Config; +import com.io.yaohun.myitems.util.MessageUtil; + +public enum TierType { + + COMMON("普通","§f", MessageUtil.getMessage("COMMON")), + UNCOMMON("稀有","§b",MessageUtil.getMessage("UNCOMMON")), + SUPERUNCOMMON("超稀有","§e",MessageUtil.getMessage("SUPERUNCOMMON")), + EPIC("史诗","§5",MessageUtil.getMessage("EPIC")), + LEGENDARY("传说","§6",MessageUtil.getMessage("LEGENDARY")), + EMINENT("卓越","§d",MessageUtil.getMessage("EMINENT")), + SANCTITY("神圣","§c",MessageUtil.getMessage("SANCTITY")); + + private final String displayName; + private final String color; + private final String character; + + TierType(String displayName, String color, String character) { + this.displayName = displayName; + this.color = color; + this.character = character; + } + + public String getDisplayName() { + return color+displayName; + } + + public String getCharacter() { + return character; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/builder/ItemTemplateBuilder.java b/src/main/java/com/io/yaohun/myitems/builder/ItemTemplateBuilder.java new file mode 100644 index 0000000..a2e51ef --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/builder/ItemTemplateBuilder.java @@ -0,0 +1,84 @@ +package com.io.yaohun.myitems.builder; + +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.TierType; +import com.io.yaohun.myitems.data.ItemTemplate; +import com.io.yaohun.myitems.manage.ItemManager; +import com.io.yaohun.myitems.util.DemonAPI; +import com.io.yaohun.myitems.util.ItemAttributeEditor; +import com.io.yaohun.myitems.util.RandomUtil; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ItemTemplateBuilder { + + private static final String EMPTY_LINE_LORE = " "; + + public static ItemStack build(String itemId) { + if(itemId == null){ + return null; + } + ItemTemplate template = ItemManager.getItemTemplate(itemId); + if(template == null){ + return null; + } + ItemStack itemStack = new ItemStack(template.getMaterial()); + if(!DemonAPI.itemIsNull(template.getItemStack())){ + itemStack = template.getItemStack(); + } + TierType tierType = template.getRandomTierType(); + ItemMeta meta = itemStack.getItemMeta(); + String itemName = template.getDisplayName().replace("{tier_name}", tierType.getDisplayName()); + + meta.setDisplayName(DemonAPI.convertHexColor(itemName)); + int gemLine = 0; + List lore = new ArrayList<>(); + if(!template.getLore().isEmpty()){ + List lores = new ArrayList<>(template.getLore()); + for (int i = 0; i < lores.size(); i++){ + String line = DemonAPI.convertHexColor(lores.get(i)); + line = line.replace("{tier_name}", tierType.getDisplayName()); + line = line.replace("{tier_character}", tierType.getCharacter()); + line = line.replace("{item_icon}", template.getItemIcon()); + if(line.contains("{GemSlot}")) { + line = line.replace("{GemSlot}", EMPTY_LINE_LORE); + gemLine = i; + } + lores.set(i, line); + } + lore.addAll(lores); + } + meta.setLore(lore); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + itemStack.setItemMeta(meta); + + if (template.getEquipSlotType() != null) { + ItemAttributeEditor.setEquipSlot(itemStack, template.getEquipSlotType().name()); + } + ItemAttributeEditor.setTemplateId(itemStack, template.getItemId()); + ItemAttributeEditor.setSuitId(itemStack, template.getSuitId()); + + Map attributes = template.getAttributes(tierType); + if(!attributes.isEmpty()) { + for (AttributeType type : attributes.keySet()) { + ItemAttributeEditor.setAttribute(itemStack, type, attributes.get(type).getRandomValue()); + } + } + + int max = template.getMaxDurability(tierType); + if(max > 0) { + int current = template.getCurrentDurability(tierType); + ItemAttributeEditor.setDurability(itemStack, RandomUtil.getRandomInt(current, max), max); + LoreBuilder.updateDurabilityAttributeLine(itemStack); + } + return itemStack; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/builder/LoreBuilder.java b/src/main/java/com/io/yaohun/myitems/builder/LoreBuilder.java new file mode 100644 index 0000000..0e1dc75 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/builder/LoreBuilder.java @@ -0,0 +1,172 @@ +package com.io.yaohun.myitems.builder; + +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.util.*; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class LoreBuilder { + public static void updateDurabilityAttributeLine(ItemStack item){ + if(DemonAPI.itemIsNull(item) || DemonAPI.itemIsLore(item)){ + return; + } + ItemMeta meta = item.getItemMeta(); + List lore = meta.getLore(); + int max = ItemAttributeUtil.getMaxDurability(item); + if(max <= 0){ + return; + } + String color; + int current = ItemAttributeUtil.getCurrentDurability(item); + double percent = current * 1.0 / max; + if(current <= 0){ + color = "§c"; + }else if(percent > 0.5){ + color = "§a"; + }else if(percent > 0.2){ + color = "§e"; + }else { + color = "§c"; + } + AttributeType type = AttributeType.EQUIPMENT_DURABILITY; + if (Math.abs(current) >= 0) { + boolean updated = false; + String prefix = MessageUtil.getMessage(type.name()); + String newLore = DemonAPI.convertHexColor(prefix+" "+color+ AttributeFormatUtil.format(type, current)+"§7/§a"+ max); + for (int i = 0; i < lore.size(); i++) { + String line = lore.get(i); + if (line.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT))) { + lore.set(i, newLore); + updated = true; + break; + } + } + if(!updated){ + lore.add(newLore); + } + meta.setLore(lore); + item.setItemMeta(meta); + } + } + + public static void updateLore(ItemStack item) { + if(DemonAPI.itemIsNull(item)){ + return; + } + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return; + } + List lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); + + addAttributeLine(item, lore, AttributeType.PHYSICAL_ATTACK); + addAttributeLine(item, lore, AttributeType.PHYSICAL_DEFENSE); + addAttributeLine(item, lore, AttributeType.PVE_ATTACK); + addAttributeLine(item, lore, AttributeType.PVE_DEFENSE); + addAttributeLine(item, lore, AttributeType.MAX_HEALTH_BONUS); + addAttributeLine(item, lore, AttributeType.CRIT_CHANCE); + addAttributeLine(item, lore, AttributeType.CRIT_DAMAGE); + addAttributeLine(item, lore, AttributeType.HIT_CHANCE); + addAttributeLine(item, lore, AttributeType.DODGE_CHANCE); + addAttributeLine(item, lore, AttributeType.BLOCK_CHANCE); + addAttributeLine(item, lore, AttributeType.VAMPIRE_CHANCE); + addAttributeLine(item, lore, AttributeType.PENETRATION); + addAttributeLine(item, lore, AttributeType.MOVE_SPEED); + + meta.setLore(lore); + item.setItemMeta(meta); + } + + private static void addAttributeLine(ItemStack item, List lore, AttributeType type) { + double rawValue = readRawAttribute(item, type); + if (Math.abs(rawValue) <= 0.0001D) { + return; + } + + String valueText; + if (type == AttributeType.CRIT_DAMAGE) { + valueText = formatCritDamage(rawValue); + } else { + valueText = AttributeFormatUtil.format(type, rawValue); + } + + boolean updated = false; + String prefix = MessageUtil.getMessage(type.name()); + String newLore = DemonAPI.convertHexColor(prefix+" "+valueText); + for (int i = 0; i < lore.size(); i++){ + String line = lore.get(i); + if (line.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT))) { + lore.set(i, newLore); + updated = true; + break; + } + } + if(!updated){ + lore.add(newLore); + } + } + + /** + * 注意:Lore展示原始属性,不受破损屏蔽 + * 否则破损后 lore 上所有属性会消失,看起来像装备没词条 + */ + private static double readRawAttribute(ItemStack item, AttributeType type) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return 0.0D; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return 0.0D; + } + switch (type) { + case PHYSICAL_ATTACK: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.PHYSICAL_ATTACK, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case PHYSICAL_DEFENSE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.PHYSICAL_DEFENSE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case PVE_ATTACK: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.PVE_ATTACK, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case PVE_DEFENSE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.PVE_DEFENSE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case MAX_HEALTH_BONUS: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.MAX_HEALTH_BONUS, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case CRIT_CHANCE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.CRIT_CHANCE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case CRIT_DAMAGE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.CRIT_DAMAGE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case HIT_CHANCE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.HIT_CHANCE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case DODGE_CHANCE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.DODGE_CHANCE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case BLOCK_CHANCE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.BLOCK_CHANCE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case VAMPIRE_CHANCE: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.VAMPIRE_CHANCE, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case PENETRATION: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.PENETRATION, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + case MOVE_SPEED: + return meta.getPersistentDataContainer().getOrDefault(AttributeKeys.MOVE_SPEED, org.bukkit.persistence.PersistentDataType.DOUBLE, 0.0D); + default: + return 0.0D; + } + } + + private static String formatCritDamage(double value) { + double multiplier = value / 100.0; + + if (Math.abs(multiplier - Math.round(multiplier)) < 0.001D) { + return (int) multiplier + "x"; + } + + return String.format("%.2fx", multiplier) + .replace(".00x", "x") + .replace("0x", "x"); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/commands/AttCommand.java b/src/main/java/com/io/yaohun/myitems/commands/AttCommand.java new file mode 100644 index 0000000..75ab034 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/commands/AttCommand.java @@ -0,0 +1,189 @@ +package com.io.yaohun.myitems.commands; + +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.util.ItemAttributeEditor; +import com.io.yaohun.myitems.util.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class AttCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("§c此命令仅限玩家身份执行"); + return true; + } + if(!player.hasPermission("admin.use")){ + return true; + } + if (args.length < 1) { + sendHelp(player, label); + return true; + } + ItemStack item = player.getInventory().getItemInMainHand(); + if (item == null || item.getType() == Material.AIR) { + MessageUtil.sendMessageKey(player, "MainHand_Empty", Sound.ENTITY_BLAZE_DEATH); + return true; + } + if ("stats".equalsIgnoreCase(args[0])) { + if (args.length != 3) { + MessageUtil.sendDescMessage(player, "/att stats <参数> ", Sound.ENTITY_BLAZE_DEATH); + return true; + } + String attTypeName = args[1]; + String valueText = args[2]; + + AttributeType attributeType; + try { + attributeType = AttributeType.valueOf(attTypeName.toUpperCase()); + } catch (IllegalArgumentException e) { + MessageUtil.sendDescMessage(player, "/att stats <参数> ", Sound.ENTITY_BLAZE_DEATH); + return true; + } + + double value; + try { + value = Double.parseDouble(valueText); + } catch (NumberFormatException e) { + MessageUtil.sendDescMessage(player, "/att stats <参数> ", Sound.ENTITY_BLAZE_DEATH); + return true; + } + ItemAttributeEditor.setAttribute(item, attributeType, value); + + String message = MessageUtil.getMessage("Stats_Success").replace("{stats}",attributeType.name()); + message = message.replace("{value}",String.valueOf( value)); + MessageUtil.sendMessage(player, message, Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + if ("durability".equalsIgnoreCase(args[0])) { + if(args.length != 3){ + MessageUtil.sendDescMessage(player, "/att durability <当前耐久> <最大耐久>", Sound.ENTITY_BLAZE_DEATH); + return true; + } + int current; + int max; + try { + current = Integer.parseInt(args[1]); + max = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + MessageUtil.sendMessageKey(player, "Invalid_Value", Sound.ENTITY_BLAZE_DEATH); + return true; + } + + if (current < 0 || max <= 0) { + MessageUtil.sendDescMessage(player, "/att durability 128 512", Sound.ENTITY_BLAZE_DEATH); + return true; + } + if (current > max) { + current = max; + } + ItemAttributeEditor.setDurability(item, current, max); + String message = MessageUtil.getMessage("Durability_Success").replace("{now}",String.valueOf(current)); + message = message.replace("{max}",String.valueOf(max)); + MessageUtil.sendMessage(player, message, Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + if ("slot".equalsIgnoreCase(args[0])) { + if (args.length != 2) { + MessageUtil.sendDescMessage(player, "/att slot ", Sound.ENTITY_BLAZE_DEATH); + return true; + } + EquipmentSlotType slotType; + try { + slotType = EquipmentSlotType.valueOf(args[1].toUpperCase()); + } catch (IllegalArgumentException e) { + MessageUtil.sendMessageKey(player, "Invalid_SlotType", Sound.ENTITY_BLAZE_DEATH); + return true; + } + ItemAttributeEditor.setEquipSlot(item, args[1].toUpperCase()); + String message = MessageUtil.getMessage("Slot_Success").replace("{value}",slotType.name()); + MessageUtil.sendMessage(player, message, Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + if ("clear".equalsIgnoreCase(args[0])) { + if (args.length != 1) { + MessageUtil.sendDescMessage(player, "/att clear", Sound.ENTITY_BLAZE_DEATH); + return true; + } + ItemAttributeEditor.clearAllAttributes(item); + MessageUtil.sendMessageKey(player, "Clear_Success", Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + sendHelp(player, label); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("admin.use")) { + return Collections.emptyList(); + } + // /att <这里> + if (args.length == 1) { + List subCommands = Arrays.asList( + "stats", + "clear", + "slot", + "durability" + ); + return subCommands.stream() + .filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + // /att stats <这里> + if (args.length == 2 && "stats".equalsIgnoreCase(args[0])) { + return Arrays.stream(AttributeType.values()) + .map(Enum::name) + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + // /att stats PHYSICAL_ATTACK <这里> + if (args.length == 3 && "stats".equalsIgnoreCase(args[0])) { + + return Arrays.asList( + "10", + "50", + "100", + "500", + "1000" + ).stream() + .filter(s -> s.startsWith(args[2])) + .collect(Collectors.toList()); + } + // /att slot <这里> + if (args.length == 2 && "slot".equalsIgnoreCase(args[0])) { + + return Arrays.stream(EquipmentSlotType.values()) + .map(Enum::name) + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private void sendHelp(Player player, String label) { + player.sendMessage("§r"); + player.sendMessage("§e==----- ======= §2属性编辑 §e======= -----=="); + player.sendMessage("§2/" + label + " stats §e[stats] §b §f- §2设置属性"); + player.sendMessage("§2/" + label + " durability §e[now] §b §f- §2设置耐久"); + player.sendMessage("§2/" + label + " slot §e[value] §f- §2设置槽位类型"); + player.sendMessage("§2/" + label + " clear §f- §2清理物品属性"); + player.sendMessage("§e==----- ======= §2属性编辑 §e======= -----=="); + player.sendMessage("§r"); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/commands/MyItemCommand.java b/src/main/java/com/io/yaohun/myitems/commands/MyItemCommand.java new file mode 100644 index 0000000..8d77e30 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/commands/MyItemCommand.java @@ -0,0 +1,248 @@ +package com.io.yaohun.myitems.commands; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EntityAttributeAPI; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.builder.ItemTemplateBuilder; +import com.io.yaohun.myitems.config.Config; +import com.io.yaohun.myitems.gui.PanelGui; +import com.io.yaohun.myitems.manage.ItemManager; +import com.io.yaohun.myitems.util.ItemAttributeEditor; +import com.io.yaohun.myitems.util.ItemAttributeUtil; +import com.io.yaohun.myitems.util.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.attribute.Attribute; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.IronGolem; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class MyItemCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if(args.length == 1){ + if("gui".equalsIgnoreCase(args[0]) || "stats".equalsIgnoreCase(args[0])) { + if (!(sender instanceof Player player)) { + sender.sendMessage("§c此命令仅限玩家身份使用"); + return true; + } + PanelGui.OpenGui(player); + return true; + } + } + if(!sender.hasPermission("admin.use")){ + return true; + } + if(args.length == 1 && "reload".equalsIgnoreCase(args[0])){ + Config.reloadConfig(MyItems.inst()); + MessageUtil.init(MyItems.inst()); + ItemManager.reloadItemManager(MyItems.inst()); + MyItems.inst().getSuitManager().reloadSuitManager(MyItems.inst()); + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + MyItems.inst().getSuitManager().refreshPlayerSuitState(onlinePlayer, false); + MyItems.inst().getAttributeManager().markDirty(onlinePlayer); + MyItems.inst().getAttributeManager().refresh(onlinePlayer); + } + sender.sendMessage("§f[§6属性§f] §a配置文件已重载."); + if(sender instanceof Player player) { + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1, 1); + } + return true; + } + if(args.length >= 1 && "give".equalsIgnoreCase(args[0])){ + if(args.length == 1){ + sender.sendMessage("§f[§6属性§f] §c缺少物品Id参数."); + return true; + } + String playerName = sender.getName(); + if(args.length >= 3){ + playerName = args[2]; + } + Player player = Bukkit.getPlayer(playerName); + if(player == null){ + sender.sendMessage("§f[§6属性§f] §c目标玩家不存在."); + return true; + } + String itemId = args[1]; + ItemStack itemStack = ItemTemplateBuilder.build(itemId); + if (itemStack == null) { + sender.sendMessage("§f[§6属性§f] §c物品不存在."); + return true; + } + player.getInventory().addItem(itemStack); + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP,1,1); + return true; + } + if (!(sender instanceof Player player)) { + sender.sendMessage("§c此命令仅限玩家身份使用"); + return true; + } + if(args.length == 1 && ("nbt".equalsIgnoreCase(args[0]) + || "pdc".equalsIgnoreCase(args[0]) + || "debugnbt".equalsIgnoreCase(args[0]))){ + sendItemNbtDebug(player); + return true; + } + if(args.length == 1 && "test2".equalsIgnoreCase(args[0])){ + IronGolem ironGolem = player.getWorld().spawn(player.getLocation(), IronGolem.class); + ironGolem.setTarget(player); + ironGolem.getAttribute(Attribute.MAX_HEALTH).setBaseValue(1000.0D); + ironGolem.setHealth(ironGolem.getMaxHealth()); + EntityAttributeAPI.addEntityAttribute(ironGolem, "test", AttributeType.PHYSICAL_DEFENSE,500); + return true; + } + MyItems.inst().getAttributeManager().refresh(player); + AttributeData data = MyItems.inst().getAttributeManager().getFinalAttributes(player); + + player.sendMessage(ChatColor.GOLD + "====== 当前属性 ======"); + for (AttributeType type : AttributeType.values()) { + player.sendMessage(ChatColor.YELLOW + type.getDisplayName() + + ChatColor.WHITE + ": " + + ChatColor.GREEN + data.get(type)); + } + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("admin.use")) { + return Collections.emptyList(); + } + // /att stats <这里> + if (args.length == 1) { + return Arrays.asList("gui", "stats", "reload", "give", "nbt", "pdc", "debugnbt").stream() + .filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + if (args.length == 2 && "give".equalsIgnoreCase(args[0])) { + return ItemManager.getTemplateList().stream() + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + // /att stats PHYSICAL_ATTACK <这里> + if (args.length == 3 && "stats".equalsIgnoreCase(args[0])) { + + return Arrays.asList( + "10", + "50", + "100", + "500", + "1000" + ).stream() + .filter(s -> s.startsWith(args[2])) + .collect(Collectors.toList()); + } + // /att slot <这里> + if (args.length == 2 && "slot".equalsIgnoreCase(args[0])) { + + return Arrays.stream(EquipmentSlotType.values()) + .map(Enum::name) + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private void sendHelp(Player player, String label) { + player.sendMessage("§r"); + player.sendMessage("§e==----- ======= §2属性编辑 §e======= -----=="); + player.sendMessage("§2/" + label + " stats §e[stats] §b §f- §2设置属性"); + player.sendMessage("§2/" + label + " durability §e[now] §b §f- §2设置耐久"); + player.sendMessage("§2/" + label + " slot §e[value] §f- §2设置槽位类型"); + player.sendMessage("§2/" + label + " clear §f- §2清理物品属性"); + player.sendMessage("§e==----- ======= §2属性编辑 §e======= -----=="); + player.sendMessage("§r"); + } + + private void sendItemNbtDebug(Player player) { + ItemStack item = player.getInventory().getItemInMainHand(); + if (item == null || item.getType() == Material.AIR) { + player.sendMessage(ChatColor.RED + "[MyItems] 主手没有物品。"); + return; + } + + player.sendMessage(ChatColor.GOLD + "====== MyItems NBT/PDC 调试 ======"); + player.sendMessage(ChatColor.YELLOW + "Material: " + ChatColor.WHITE + item.getType().name()); + player.sendMessage(ChatColor.YELLOW + "Amount: " + ChatColor.WHITE + item.getAmount()); + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + player.sendMessage(ChatColor.RED + "ItemMeta: null"); + return; + } + + player.sendMessage(ChatColor.YELLOW + "DisplayName: " + ChatColor.WHITE + meta.getDisplayName()); + player.sendMessage(ChatColor.YELLOW + "LoreLines: " + ChatColor.WHITE + (meta.hasLore() ? meta.getLore().size() : 0)); + player.sendMessage(ChatColor.YELLOW + "TemplateId: " + ChatColor.WHITE + ItemAttributeUtil.readTemplateId(item)); + player.sendMessage(ChatColor.YELLOW + "SuitId: " + ChatColor.WHITE + ItemAttributeUtil.readSuitId(item)); + player.sendMessage(ChatColor.YELLOW + "Durability: " + ChatColor.WHITE + + ItemAttributeUtil.getCurrentDurability(item) + "/" + ItemAttributeUtil.getMaxDurability(item)); + player.sendMessage(ChatColor.YELLOW + "Broken: " + ChatColor.WHITE + ItemAttributeUtil.isBroken(item)); + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + if (pdc.getKeys().isEmpty()) { + player.sendMessage(ChatColor.GRAY + "PDC: 空"); + return; + } + + player.sendMessage(ChatColor.AQUA + "--- PDC Keys ---"); + for (NamespacedKey key : pdc.getKeys()) { + player.sendMessage(ChatColor.GRAY + key.toString() + + ChatColor.WHITE + " = " + + ChatColor.GREEN + readPdcValue(pdc, key)); + } + } + + private String readPdcValue(PersistentDataContainer pdc, NamespacedKey key) { + if (pdc.has(key, PersistentDataType.STRING)) { + return "STRING:" + pdc.getOrDefault(key, PersistentDataType.STRING, ""); + } + if (pdc.has(key, PersistentDataType.DOUBLE)) { + return "DOUBLE:" + pdc.getOrDefault(key, PersistentDataType.DOUBLE, 0.0D); + } + if (pdc.has(key, PersistentDataType.INTEGER)) { + return "INTEGER:" + pdc.getOrDefault(key, PersistentDataType.INTEGER, 0); + } + if (pdc.has(key, PersistentDataType.LONG)) { + return "LONG:" + pdc.getOrDefault(key, PersistentDataType.LONG, 0L); + } + if (pdc.has(key, PersistentDataType.FLOAT)) { + return "FLOAT:" + pdc.getOrDefault(key, PersistentDataType.FLOAT, 0.0F); + } + if (pdc.has(key, PersistentDataType.SHORT)) { + return "SHORT:" + pdc.getOrDefault(key, PersistentDataType.SHORT, (short) 0); + } + if (pdc.has(key, PersistentDataType.BYTE)) { + return "BYTE:" + pdc.getOrDefault(key, PersistentDataType.BYTE, (byte) 0); + } + if (pdc.has(key, PersistentDataType.INTEGER_ARRAY)) { + return "INTEGER_ARRAY:" + Arrays.toString(pdc.getOrDefault(key, PersistentDataType.INTEGER_ARRAY, new int[0])); + } + if (pdc.has(key, PersistentDataType.LONG_ARRAY)) { + return "LONG_ARRAY:" + Arrays.toString(pdc.getOrDefault(key, PersistentDataType.LONG_ARRAY, new long[0])); + } + if (pdc.has(key, PersistentDataType.BYTE_ARRAY)) { + return "BYTE_ARRAY:" + Arrays.toString(pdc.getOrDefault(key, PersistentDataType.BYTE_ARRAY, new byte[0])); + } + return "未知类型"; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/commands/RepairCommand.java b/src/main/java/com/io/yaohun/myitems/commands/RepairCommand.java new file mode 100644 index 0000000..4c8186e --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/commands/RepairCommand.java @@ -0,0 +1,123 @@ +package com.io.yaohun.myitems.commands; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.util.MessageUtil; +import com.io.yaohun.myitems.util.RepairUtil; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class RepairCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("§c此命令仅限玩家身份执行"); + return true; + } + if(!player.hasPermission("admin.use")){ + return true; + } + if (args.length < 1) { + sendHelp(player, label); + return true; + } + if ("repair".equalsIgnoreCase(args[0])) { + Player target = getTargetPlayer(player, args); + if (target == null) { + MessageUtil.sendMessage(player, "§c目标玩家不在线", Sound.ENTITY_BLAZE_DEATH); + return true; + } + + ItemStack item = target.getInventory().getItemInMainHand(); + if (item == null || item.getType() == Material.AIR) { + MessageUtil.sendMessageKey(player, "MainHand_Empty", Sound.ENTITY_BLAZE_DEATH); + return true; + } + + boolean success = RepairUtil.repairToFull(item); + if (!success) { + MessageUtil.sendMessageKey(player, "Durability_Not_Exist", Sound.ENTITY_BLAZE_DEATH); + return true; + } + refreshTarget(target); + MessageUtil.sendMessageKey(target, "Repair_Success", Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + if ("repairall".equalsIgnoreCase(args[0])) { + Player target = getTargetPlayer(player, args); + if (target == null) { + MessageUtil.sendMessage(player, "§c目标玩家不在线", Sound.ENTITY_BLAZE_DEATH); + return true; + } + + int count = RepairUtil.repairAll(target); + if (count <= 0) { + MessageUtil.sendMessageKey(player, "Durability_Not_Amount", Sound.ENTITY_BLAZE_DEATH); + return true; + } + refreshTarget(target); + MessageUtil.sendMessageKey(target, "Repair_All_Success", Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + return true; + } + sendHelp(player, label); + return true; + } + + private Player getTargetPlayer(Player player, String[] args) { + String playerName = player.getName(); + if(args.length == 2){ + playerName = args[1]; + } + return Bukkit.getPlayer(playerName); + } + + private void refreshTarget(Player target) { + MyItems.inst().getSuitManager().refreshPlayerSuitState(target, true); + MyItems.inst().getAttributeManager().markDirty(target); + MyItems.inst().getAttributeManager().refresh(target); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("admin.use")) { + return Collections.emptyList(); + } + if (args.length == 1) { + List subCommands = Arrays.asList( + "repair", + "repairall" + ); + return subCommands.stream() + .filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + if (args.length == 2 && ("repair".equalsIgnoreCase(args[0]) || "repairall".equalsIgnoreCase(args[0]))) { + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private void sendHelp(Player player, String label) { + player.sendMessage("§r"); + player.sendMessage("§e==----- ======= §2耐久修复 §e======= -----=="); + player.sendMessage("§2/" + label + " repair §e[player] §f- §2修复手持物品"); + player.sendMessage("§2/" + label + " repairall §e[player] §f- §2修复所有物品"); + player.sendMessage("§e==----- ======= §2耐久修复 §e======= -----=="); + player.sendMessage("§r"); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/config/Config.java b/src/main/java/com/io/yaohun/myitems/config/Config.java new file mode 100644 index 0000000..4b55070 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/config/Config.java @@ -0,0 +1,15 @@ +package com.io.yaohun.myitems.config; + +import com.io.yaohun.myitems.MyItems; +import org.bukkit.configuration.file.FileConfiguration; + +public class Config { + + + public static void reloadConfig(MyItems plugin){ + plugin.reloadConfig(); + plugin.saveConfig(); + FileConfiguration config = plugin.getConfig(); + + } +} diff --git a/src/main/java/com/io/yaohun/myitems/data/CEngineData.java b/src/main/java/com/io/yaohun/myitems/data/CEngineData.java new file mode 100644 index 0000000..0eda95c --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/data/CEngineData.java @@ -0,0 +1,50 @@ +package com.io.yaohun.myitems.data; + +import com.io.yaohun.myitems.util.DemonAPI; +import net.momirealms.craftengine.bukkit.api.CraftEngineItems; +import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition; +import net.momirealms.craftengine.core.util.Key; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +public class CEngineData { + + private String Namespace; + private String Value; + private ItemStack itemStack; + + public CEngineData(String Namespace, String Value) { + this.Namespace = Namespace; + this.Value = Value; + } + + public boolean isItemStack() { + + if (!DemonAPI.itemIsNull(itemStack)) { + return true; + } + + Key key = new Key(Namespace, Value); + + BukkitItemDefinition definition = CraftEngineItems.byId(key); + + // 防止空指针 + if (definition == null) { + Bukkit.getLogger().warning("[MyItems] 未找到 CraftEngine 物品: " + Namespace + ":" + Value); + return false; + } + + ItemStack items = definition.buildBukkitItem(); + + if (!DemonAPI.itemIsNull(items)) { + itemStack = items; + return true; + } + + return false; + } + + public ItemStack getItemStack() { + return itemStack; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/data/ItemTemplate.java b/src/main/java/com/io/yaohun/myitems/data/ItemTemplate.java new file mode 100644 index 0000000..9b2396f --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/data/ItemTemplate.java @@ -0,0 +1,146 @@ +package com.io.yaohun.myitems.data; + +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.api.TierType; +import com.io.yaohun.myitems.util.RandomBuilder; +import com.io.yaohun.myitems.util.RandomUtil; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ItemTemplate { + + private String itemId; + private ItemStack itemStack; + private Material material; + private String displayName; + private List lore; + + private String itemIcon; + + private EquipmentSlotType equipSlotType; + + private Map tierTypes = new HashMap<>(); + private Map tierAttributes = new HashMap<>(); + private String suitId; + + + public ItemTemplate(String itemId, String itemIcon,ItemStack itemStack, + Material material, String displayName, + List lore, EquipmentSlotType equipSlotType, + Map tierTypes, Map tierAttributes, + String suitId) { + this.itemId = itemId; + this.itemIcon = itemIcon; + this.itemStack = itemStack; + this.material = material; + this.displayName = displayName; + this.lore = lore; + this.equipSlotType = equipSlotType; + this.tierTypes = tierTypes; + this.tierAttributes = tierAttributes; + this.suitId = suitId; + } + + public String getItemId() { + return itemId; + } + + public String getItemIcon() { + return itemIcon; + } + + public Material getMaterial() { + return material; + } + + public String getDisplayName() { + return displayName; + } + + public List getLore() { + return lore; + } + + public ItemStack getItemStack() { + return itemStack; + } + + public EquipmentSlotType getEquipSlotType() { + return equipSlotType; + } + + public Map getTierTypes() { + Map chanceMap = new HashMap<>(); + for (Map.Entry entry : tierTypes.entrySet()){ + chanceMap.put(entry.getKey().name(),entry.getValue()); + } + return chanceMap; + } + + public Map getTierAttributes() { + return tierAttributes; + } + + public String getSuitId() { + return suitId; + } + + public TierType getRandomTierType() { + Map chanceMap = getTierTypes(); + RandomBuilder builder = new RandomBuilder<>(chanceMap); + return TierType.valueOf(builder.random()); + } + + public TierAttribute getTierAttribute(TierType tierType){ + return tierAttributes.get(tierType); + } + + public int getCurrentDurability(TierType tierType){ + return getTierAttribute(tierType).currentDurability; + } + + public int getMaxDurability(TierType tierType){ + return getTierAttribute(tierType).maxDurability; + } + + public Map getAttributes(TierType tierType){ + return getTierAttribute(tierType).getAttributes(); + } + + public static class TierAttribute { + + private int currentDurability; + private int maxDurability; + + private Map attributes = new HashMap<>(); + + public TierAttribute(int currentDurability, int maxDurability, Map attributes) { + this.currentDurability = currentDurability; + this.maxDurability = maxDurability; + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + } + + public static class AttributeRandomValue { + private double min; + private double max; + + public AttributeRandomValue(double min, double max) { + this.min = min; + this.max = max; + } + + public double getRandomValue(){ + return RandomUtil.getRandomDouble(min, max, 2); + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/data/SuitTemplate.java b/src/main/java/com/io/yaohun/myitems/data/SuitTemplate.java new file mode 100644 index 0000000..21bc296 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/data/SuitTemplate.java @@ -0,0 +1,50 @@ +package com.io.yaohun.myitems.data; + +import com.io.yaohun.myitems.api.AttributeType; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class SuitTemplate { + + private final String suitId; + private final String displayName; + private final int requireAmount; + private final Set itemIds; + private final Map attributes; + + public SuitTemplate(String suitId, String displayName, int requireAmount, + Set itemIds, Map attributes) { + this.suitId = suitId; + this.displayName = displayName; + this.requireAmount = requireAmount; + this.itemIds = new HashSet<>(itemIds); + this.attributes = attributes; + } + + public String getSuitId() { + return suitId; + } + + public String getDisplayName() { + return displayName; + } + + public int getRequireAmount() { + return requireAmount; + } + + public Set getItemIds() { + return Collections.unmodifiableSet(itemIds); + } + + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + public boolean containsItem(String itemId) { + return itemIds.contains(itemId); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/engine/DamageEngine.java b/src/main/java/com/io/yaohun/myitems/engine/DamageEngine.java new file mode 100644 index 0000000..7f73876 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/engine/DamageEngine.java @@ -0,0 +1,127 @@ +package com.io.yaohun.myitems.engine; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.DamageResult; +import com.io.yaohun.myitems.api.EntityAttributeAPI; +import com.io.yaohun.myitems.manage.AttributeManager; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.util.concurrent.ThreadLocalRandom; + +public class DamageEngine { + + private static final double PVE_DEFENSE_CONSTANT = 1000.0; + private static final double PVP_DEFENSE_CONSTANT = 1000.0; + private static final double BLOCK_DAMAGE_RATE = 0.5D; + private static final double BLOCK_REFLECT_RATE = 0.1D; + private static final double VAMPIRE_HEAL_RATE = 0.03D; + + private final AttributeManager attributeManager; + + public DamageEngine(AttributeManager attributeManager) { + this.attributeManager = attributeManager; + } + + public DamageResult calculate(LivingEntity attacker, LivingEntity target) { + AttributeData attackerData = getEntityAttributes(attacker); + AttributeData targetData = getEntityAttributes(target); + + boolean miss = isMiss(attackerData, targetData); + if (miss) { + return new DamageResult(true, false, 0); + } + + boolean penetration = isTriggered(attackerData.get(AttributeType.PENETRATION)); + double baseDamage = calculateBaseDamage(attacker, target, attackerData, targetData, penetration); + + boolean crit = isCritical(attackerData); + double finalDamage = baseDamage; + if (crit) { + double critDamage = attackerData.get(AttributeType.CRIT_DAMAGE); + finalDamage = finalDamage * (1.0 + (critDamage / 100.0)); + } + + boolean block = isTriggered(targetData.get(AttributeType.BLOCK_CHANCE)); + double reflectDamage = 0.0D; + if (block) { + finalDamage = finalDamage * BLOCK_DAMAGE_RATE; + reflectDamage = finalDamage * BLOCK_REFLECT_RATE; + } + + if (finalDamage < 1.0) { + finalDamage = 1.0; + } + + boolean vampire = isTriggered(attackerData.get(AttributeType.VAMPIRE_CHANCE)); + double healAmount = vampire ? finalDamage * VAMPIRE_HEAL_RATE : 0.0D; + + return new DamageResult(false, crit, penetration, block, vampire, finalDamage, reflectDamage, healAmount); + } + + private boolean isMiss(AttributeData attackerData, AttributeData targetData) { + double hitChance = attackerData.get(AttributeType.HIT_CHANCE); + double dodgeChance = targetData.get(AttributeType.DODGE_CHANCE); + + double finalHitRate = 90.0 + hitChance - dodgeChance; + finalHitRate = clamp(finalHitRate, 20.0, 100.0); + + double roll = ThreadLocalRandom.current().nextDouble(100.0); + return roll >= finalHitRate; + } + + private boolean isCritical(AttributeData attackerData) { + return isTriggered(attackerData.get(AttributeType.CRIT_CHANCE)); + } + + private double calculateBaseDamage(LivingEntity attacker, LivingEntity target, + AttributeData attackerData, AttributeData targetData, + boolean penetration) { + + double attack = attackerData.get(AttributeType.PHYSICAL_ATTACK); + double defense = targetData.get(AttributeType.PHYSICAL_DEFENSE); + + boolean isPve = isPveBattle(attacker, target); + boolean isPvp = isPvpBattle(attacker, target); + + if (isPve) { + attack += attackerData.get(AttributeType.PVE_ATTACK); + defense += targetData.get(AttributeType.PVE_DEFENSE); + } + + double constant = isPvp ? PVP_DEFENSE_CONSTANT : PVE_DEFENSE_CONSTANT; + + double effectiveDefense = penetration ? 0.0D : Math.max(0.0, defense); + double damage = attack * constant / (constant + effectiveDefense); + + return Math.max(1.0, damage); + } + + private boolean isPveBattle(LivingEntity attacker, LivingEntity target) { + return (attacker instanceof Player && !(target instanceof Player)) + || (!(attacker instanceof Player) && target instanceof Player); + } + + private boolean isPvpBattle(LivingEntity attacker, LivingEntity target) { + return attacker instanceof Player + && target instanceof Player; + } + + private AttributeData getEntityAttributes(LivingEntity entity) { + if (entity instanceof Player player) { + return attributeManager.getFinalAttributes(player); + } + AttributeData data = EntityAttributeAPI.getFinalEntityAttribute(entity); + return data == null ? new AttributeData() : data; + } + + private boolean isTriggered(double chance) { + double fixedChance = clamp(chance, 0.0D, 100.0D); + return ThreadLocalRandom.current().nextDouble(100.0D) < fixedChance; + } + + private double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/gui/PanelGui.java b/src/main/java/com/io/yaohun/myitems/gui/PanelGui.java new file mode 100644 index 0000000..3107773 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/gui/PanelGui.java @@ -0,0 +1,136 @@ +package com.io.yaohun.myitems.gui; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.util.DemonAPI; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +public class PanelGui { + + public static String invTitle = "§9§l个人属性菜单"; + + public static void OpenGui(Player player){ + AttributeData data = MyItems.inst().getAttributeManager().getFinalAttributes(player); + Inventory inv = Bukkit.createInventory(null,54,invTitle); + for (int slot = 0; slot < 54; slot++){ + inv.setItem(slot, blackGlass()); + } + for (int slot : new int[]{0,8,9,17,18,26,27,35,36,44,45,53}){ + inv.setItem(slot, whiteGlass()); + } + PlayerInventory pInv = player.getInventory(); + int helmetSlot = 11; + if(!DemonAPI.itemIsNull(pInv.getHelmet())){ + inv.setItem(helmetSlot, pInv.getHelmet()); + } else { + inv.setItem(helmetSlot, slotRedGlass("§c头盔槽")); + } + int chestplateSlot = 20; + if(!DemonAPI.itemIsNull(pInv.getChestplate())){ + inv.setItem(chestplateSlot, pInv.getChestplate()); + } else { + inv.setItem(chestplateSlot, slotRedGlass("§c胸甲槽")); + } + int leggingsSlot = 29; + if(!DemonAPI.itemIsNull(pInv.getLeggings())){ + inv.setItem(leggingsSlot, pInv.getLeggings()); + } else { + inv.setItem(leggingsSlot, slotRedGlass("§c护腿槽")); + } + int bootsSlot = 38; + if(!DemonAPI.itemIsNull(pInv.getBoots())){ + inv.setItem(bootsSlot, pInv.getBoots()); + } else { + inv.setItem(bootsSlot, slotRedGlass("§c战靴槽")); + } + int mainHandSlot = 13; + if(!DemonAPI.itemIsNull(pInv.getItemInMainHand())){ + inv.setItem(mainHandSlot, pInv.getItemInMainHand()); + } else { + inv.setItem(mainHandSlot, slotRedGlass("§c主手槽")); + } + int offHandSlot = 22; + if(!DemonAPI.itemIsNull(pInv.getItemInOffHand())){ + inv.setItem(offHandSlot, pInv.getItemInOffHand()); + } else { + inv.setItem(offHandSlot, slotRedGlass("§c副手槽")); + } + inv.setItem(15, attackAttribute(data)); + inv.setItem(24, defenseAttribute(data)); + player.openInventory(inv); + } + + private static ItemStack attackAttribute(AttributeData data){ + ItemStack stack = new ItemStack(Material.IRON_SWORD); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§8[§e§l攻击属性§8]"); + List lore = new ArrayList<>(); + lore.add("§8§m------------------"); + lore.add(" "); + lore.add("§C§l➨ §2物理攻击: §7"+data.get(AttributeType.PHYSICAL_ATTACK)); + lore.add("§C§l➨ §2PVE攻击: §7"+data.get(AttributeType.PVE_ATTACK)); + lore.add("§C§l➨ §2暴击概率: §7"+data.get(AttributeType.CRIT_CHANCE)+"%"); + lore.add("§C§l➨ §2暴击伤害: §7"+String.format("%.1f", data.get(AttributeType.CRIT_DAMAGE) / 100.0)+"x"); + lore.add("§C§l➨ §2命中概率: §7"+data.get(AttributeType.HIT_CHANCE)+"%"); + lore.add("§C§l➨ §2护甲穿透: §7"+data.get(AttributeType.PENETRATION)+"%"); + lore.add("§C§l➨ §2吸血概率: §7"+data.get(AttributeType.VAMPIRE_CHANCE)+"%"); + meta.setLore(lore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + stack.setItemMeta(meta); + return stack; + } + + private static ItemStack defenseAttribute(AttributeData data){ + ItemStack stack = new ItemStack(Material.IRON_CHESTPLATE); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§8[§e§l防御属性§8]"); + List lore = new ArrayList<>(); + lore.add("§8§m------------------"); + lore.add(" "); + lore.add("§C§l➨ §2物理防御: §7"+data.get(AttributeType.PHYSICAL_DEFENSE)); + lore.add("§C§l➨ §2PVE防御: §7"+data.get(AttributeType.PVE_DEFENSE)); + lore.add("§C§l➨ §2生命提升: §7"+data.get(AttributeType.MAX_HEALTH_BONUS)); + lore.add("§C§l➨ §2闪避概率: §7"+data.get(AttributeType.DODGE_CHANCE)+"%"); + lore.add("§C§l➨ §2格挡概率: §7"+data.get(AttributeType.BLOCK_CHANCE)+"%"); + lore.add("§C§l➨ §2移速加成: §7"+data.get(AttributeType.MOVE_SPEED)+"%"); + meta.setLore(lore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + stack.setItemMeta(meta); + return stack; + } + + private static ItemStack whiteGlass(){ + ItemStack stack = new ItemStack(Material.WHITE_STAINED_GLASS_PANE); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§7§l[§e§l!§7§l]"); + stack.setItemMeta(meta); + return stack; + } + + private static ItemStack blackGlass(){ + ItemStack stack = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§7§l[§e§l!§7§l]"); + stack.setItemMeta(meta); + return stack; + } + + private static ItemStack slotRedGlass(String name){ + ItemStack stack = new ItemStack(Material.RED_STAINED_GLASS_PANE); + ItemMeta meta = stack.getItemMeta(); + meta.setDisplayName("§8["+name+"§8]"); + stack.setItemMeta(meta); + return stack; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/DamageListener.java b/src/main/java/com/io/yaohun/myitems/listener/DamageListener.java new file mode 100644 index 0000000..371e0e6 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/DamageListener.java @@ -0,0 +1,126 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.api.DamageResult; +import com.io.yaohun.myitems.engine.DamageEngine; +import com.io.yaohun.myitems.util.EquipmentCheckUtil; +import org.bukkit.ChatColor; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +public class DamageListener implements Listener { + + private final DamageEngine damageEngine; + + public DamageListener(DamageEngine damageEngine) { + this.damageEngine = damageEngine; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onDamage(EntityDamageByEntityEvent event) { + Entity rawDamager = event.getDamager(); + Entity rawTarget = event.getEntity(); + + LivingEntity attacker = getRealDamager(rawDamager); + if (!(rawTarget instanceof LivingEntity target)) { + return; + } + + if (attacker == null) { + return; + } + + if (attacker instanceof Player player) { + if (EquipmentCheckUtil.isBrokenCustomWeapon(player)) { + player.sendActionBar(ChatColor.RED + "武器已破损,无法发挥属性效果"); + return; + } + } + + DamageResult result = damageEngine.calculate(attacker, target); + + if (result.isMiss()) { + event.setDamage(0); + event.setCancelled(true); + + if (attacker instanceof Player player) { + player.sendActionBar(ChatColor.GRAY + "Miss"); + } + if (target instanceof Player player) { + player.sendActionBar(ChatColor.YELLOW + "闪避"); + } + return; + } + + event.setDamage(result.getDamage()); + applyVampire(attacker, result); + applyReflect(attacker, result); + sendBattleMessage(attacker, target, result); + } + + private void applyVampire(LivingEntity attacker, DamageResult result) { + if (!result.isVampire() || result.getHealAmount() <= 0) { + return; + } + + AttributeInstance maxHealth = attacker.getAttribute(Attribute.MAX_HEALTH); + if (maxHealth == null) { + return; + } + + double newHealth = Math.min(maxHealth.getValue(), attacker.getHealth() + result.getHealAmount()); + attacker.setHealth(newHealth); + } + + private void applyReflect(LivingEntity attacker, DamageResult result) { + if (!result.isBlock() || result.getReflectDamage() <= 0 || attacker.isDead()) { + return; + } + + attacker.damage(result.getReflectDamage()); + } + + private void sendBattleMessage(LivingEntity attacker, LivingEntity target, DamageResult result) { + if (attacker instanceof Player player) { + StringBuilder message = new StringBuilder(); + if (result.isCrit()) { + message.append(ChatColor.RED).append("暴击! "); + } else { + message.append(ChatColor.WHITE).append("伤害 "); + } + message.append(ChatColor.RED).append(String.format("%.1f", result.getDamage())); + if (result.isPenetration()) { + message.append(ChatColor.GRAY).append(" 穿透"); + } + if (result.isVampire()) { + message.append(ChatColor.DARK_RED).append(" 吸血"); + } + player.sendActionBar(message.toString()); + } + + if (target instanceof Player player && result.isBlock()) { + player.sendActionBar(ChatColor.YELLOW + "格挡 -" + ChatColor.RED + String.format("%.1f", result.getDamage())); + } + } + + private LivingEntity getRealDamager(Entity entity) { + if (entity instanceof LivingEntity livingEntity) { + return livingEntity; + } + + if (entity instanceof Arrow arrow) { + if (arrow.getShooter() instanceof LivingEntity shooter) { + return shooter; + } + } + + return null; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/DurabilityListener.java b/src/main/java/com/io/yaohun/myitems/listener/DurabilityListener.java new file mode 100644 index 0000000..89a3a46 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/DurabilityListener.java @@ -0,0 +1,153 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.util.ItemAttributeUtil; +import com.io.yaohun.myitems.util.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Sound; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.inventory.ItemStack; + +public class DurabilityListener implements Listener { + + private final AttributeManager attributeManager; + + public DurabilityListener(AttributeManager attributeManager) { + this.attributeManager = attributeManager; + } + + /** + * 玩家攻击时:主手武器掉耐久 + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerAttack(EntityDamageByEntityEvent event) { + LivingEntity damager = getRealDamager(event.getDamager()); + if (!(damager instanceof Player player)) { + return; + } + + if (!(event.getEntity() instanceof LivingEntity)) { + return; + } + + boolean changed = false; + ItemStack weapon = player.getInventory().getItemInMainHand(); + if (weapon != null && ItemAttributeUtil.hasDurability(weapon)) { + boolean wasBroken = ItemAttributeUtil.isBroken(weapon); + + ItemAttributeUtil.damageDurability(weapon, 1); + changed = true; + + boolean isBroken = ItemAttributeUtil.isBroken(weapon); + if (!wasBroken && isBroken) { + sendBrokenMessage(player, "Broken_MainHand"); + } + } + + if (changed) { + refreshLater(player); + } + } + + /** + * 玩家受击时:副手 + 四件护甲掉耐久 + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerDamaged(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player player)) { + return; + } + + boolean changed = false; + + changed |= damageOffhand(player); + changed |= damageArmorPiece(player, player.getInventory().getHelmet(), "Broken_Helmet"); + changed |= damageArmorPiece(player, player.getInventory().getChestplate(), "Broken_Chestplate"); + changed |= damageArmorPiece(player, player.getInventory().getLeggings(), "Broken_Leggings"); + changed |= damageArmorPiece(player, player.getInventory().getBoots(), "Broken_Boots"); + + if (changed) { + refreshLater(player); + } + } + + private boolean damageOffhand(Player player) { + ItemStack offhand = player.getInventory().getItemInOffHand(); + if (offhand == null) { + return false; + } + + if (!ItemAttributeUtil.hasDurability(offhand)) { + return false; + } + + boolean wasBroken = ItemAttributeUtil.isBroken(offhand); + + ItemAttributeUtil.damageDurability(offhand, 1); + + boolean isBroken = ItemAttributeUtil.isBroken(offhand); + if (!wasBroken && isBroken) { + sendBrokenMessage(player, "Broken_Offhand"); + } + + return true; + } + + private boolean damageArmorPiece(Player player, ItemStack armor, String key) { + if (armor == null) { + return false; + } + + if (!ItemAttributeUtil.hasDurability(armor)) { + return false; + } + + boolean wasBroken = ItemAttributeUtil.isBroken(armor); + + ItemAttributeUtil.damageDurability(armor, 1); + + boolean isBroken = ItemAttributeUtil.isBroken(armor); + if (!wasBroken && isBroken) { + sendBrokenMessage(player, key); + } + + return true; + } + + private void sendBrokenMessage(Player player, String key) { + MessageUtil.sendMessageKey(player, key, Sound.ITEM_SHIELD_BREAK); + } + + private LivingEntity getRealDamager(Entity entity) { + if (entity instanceof LivingEntity livingEntity) { + return livingEntity; + } + + if (entity instanceof Arrow arrow) { + if (arrow.getShooter() instanceof LivingEntity shooter) { + return shooter; + } + } + + return null; + } + + private void refreshLater(Player player) { + attributeManager.markDirty(player); + + Bukkit.getScheduler().runTaskLater(MyItems.inst(), () -> { + if (player.isOnline()) { + attributeManager.refresh(player); + } + }, 1L); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/EntityCleanupListener.java b/src/main/java/com/io/yaohun/myitems/listener/EntityCleanupListener.java new file mode 100644 index 0000000..e13700a --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/EntityCleanupListener.java @@ -0,0 +1,33 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.manage.EntityAttributeStorage; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.world.EntitiesUnloadEvent; + +public class EntityCleanupListener implements Listener { + + private final EntityAttributeStorage storage; + + public EntityCleanupListener(EntityAttributeStorage storage) { + this.storage = storage; + } + + @EventHandler + public void onDeath(EntityDeathEvent event) { + if (event.getEntity() instanceof LivingEntity livingEntity) { + storage.clearEntity(livingEntity.getUniqueId()); + } + } + + @EventHandler + public void onEntitiesUnload(EntitiesUnloadEvent event) { + event.getEntities().forEach(entity -> { + if (entity instanceof LivingEntity livingEntity) { + storage.clearEntity(livingEntity.getUniqueId()); + } + }); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/PlayerEquipListener.java b/src/main/java/com/io/yaohun/myitems/listener/PlayerEquipListener.java new file mode 100644 index 0000000..61c92eb --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/PlayerEquipListener.java @@ -0,0 +1,153 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.util.DemonAPI; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.*; +import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; + +public class PlayerEquipListener implements Listener { + + private final AttributeManager attributeManager; + + public PlayerEquipListener(AttributeManager attributeManager) { + this.attributeManager = attributeManager; + } + + private boolean isArmor(ItemStack item) { + String name = item.getType().name(); + + return name.endsWith("_HELMET") + || name.endsWith("_CHESTPLATE") + || name.endsWith("_LEGGINGS") + || name.endsWith("_BOOTS"); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onJoin(PlayerJoinEvent event) { + scheduleRefresh(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + if (shouldRefresh(event)) { + scheduleRefresh(player); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + scheduleRefresh(player); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSwap(PlayerSwapHandItemsEvent event) { + scheduleRefresh(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHeldChange(PlayerItemHeldEvent event) { + scheduleRefresh(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onRespawn(PlayerRespawnEvent event) { + scheduleRefresh(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onDeath(PlayerDeathEvent event) { + scheduleRefresh(event.getEntity()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onBreak(PlayerItemBreakEvent event) { + scheduleRefresh(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldChange(PlayerChangedWorldEvent event) { + scheduleRefresh(event.getPlayer()); + } + + private boolean shouldRefresh(InventoryClickEvent event) { + InventoryType.SlotType slotType = event.getSlotType(); + ClickType click = event.getClick(); + InventoryAction action = event.getAction(); + + // 只要碰到护甲槽 / 快捷栏 / 副手相关,优先刷新 + if (slotType == InventoryType.SlotType.ARMOR) { + return true; + } + + if (slotType == InventoryType.SlotType.QUICKBAR) { + return true; + } + + if (slotType == InventoryType.SlotType.CONTAINER) { + // 普通容器也有可能通过 shift 或热键把装备切上去 + } + + // 数字键切换 + if (click == ClickType.NUMBER_KEY) { + return true; + } + + // Shift 快速移动 + if (click == ClickType.SHIFT_LEFT || click == ClickType.SHIFT_RIGHT) { + return true; + } + + // 常见影响装备栏的动作 + switch (action) { + case HOTBAR_SWAP: + case MOVE_TO_OTHER_INVENTORY: + case SWAP_WITH_CURSOR: + case PLACE_ALL: + case PLACE_ONE: + case PLACE_SOME: + case PICKUP_ALL: + case PICKUP_HALF: + case PICKUP_ONE: + case PICKUP_SOME: + case COLLECT_TO_CURSOR: + return true; + default: + return false; + } + } + + private void scheduleRefresh(Player player) { + if (player == null) { + return; + } + + attributeManager.markDirty(player); + + Bukkit.getScheduler().runTaskLater(MyItems.inst(), () -> { + if (!player.isOnline()) { + return; + } + + attributeManager.refresh(player); + }, 2L); + } + +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/PlayerJoinListener.java b/src/main/java/com/io/yaohun/myitems/listener/PlayerJoinListener.java new file mode 100644 index 0000000..e51d416 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/PlayerJoinListener.java @@ -0,0 +1,52 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.gui.PanelGui; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.manage.CacheManager; +import com.io.yaohun.myitems.util.SpeedUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; + +public class PlayerJoinListener implements Listener { + + private final AttributeManager attributeManager; + private final CacheManager cacheManager; + + public PlayerJoinListener(AttributeManager attributeManager, CacheManager cacheManager) { + this.attributeManager = attributeManager; + this.cacheManager = cacheManager; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Bukkit.getScheduler().runTaskLater(MyItems.inst(), () -> { + if (!player.isOnline()) { + return; + } + attributeManager.markDirty(player); + attributeManager.refresh(player); + }, 20L); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + SpeedUtil.reset(player); + cacheManager.remove(player.getUniqueId()); + } + + @EventHandler + public void onClick(InventoryClickEvent e){ + if(e.getView().getTitle().equalsIgnoreCase(PanelGui.invTitle)){ + e.setCancelled(true); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/io/yaohun/myitems/listener/SuitListener.java b/src/main/java/com/io/yaohun/myitems/listener/SuitListener.java new file mode 100644 index 0000000..9ed77d0 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/SuitListener.java @@ -0,0 +1,143 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.manage.SuitManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerItemBreakEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; + +public class SuitListener implements Listener { + + private final SuitManager suitManager; + private final AttributeManager attributeManager; + + public SuitListener(SuitManager suitManager, AttributeManager attributeManager) { + this.suitManager = suitManager; + this.attributeManager = attributeManager; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onJoin(PlayerJoinEvent event) { + scheduleCheck(event.getPlayer(), false, 2L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onQuit(PlayerQuitEvent event) { + suitManager.clearPlayerState(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + if (shouldCheck(event)) { + scheduleCheck(player, true, 2L); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getWhoClicked() instanceof Player player) { + scheduleCheck(player, true, 2L); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSwap(PlayerSwapHandItemsEvent event) { + scheduleCheck(event.getPlayer(), true, 2L); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHeldChange(PlayerItemHeldEvent event) { + scheduleCheck(event.getPlayer(), true, 2L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onRespawn(PlayerRespawnEvent event) { + scheduleCheck(event.getPlayer(), true, 2L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onDeath(PlayerDeathEvent event) { + scheduleCheck(event.getEntity(), true, 2L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onBreak(PlayerItemBreakEvent event) { + scheduleCheck(event.getPlayer(), true, 1L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldChange(PlayerChangedWorldEvent event) { + scheduleCheck(event.getPlayer(), true, 2L); + } + + private boolean shouldCheck(InventoryClickEvent event) { + InventoryType.SlotType slotType = event.getSlotType(); + ClickType click = event.getClick(); + InventoryAction action = event.getAction(); + + if (slotType == InventoryType.SlotType.ARMOR || slotType == InventoryType.SlotType.QUICKBAR) { + return true; + } + + if (click == ClickType.NUMBER_KEY || click == ClickType.SHIFT_LEFT || click == ClickType.SHIFT_RIGHT) { + return true; + } + + switch (action) { + case HOTBAR_SWAP: + case MOVE_TO_OTHER_INVENTORY: + case SWAP_WITH_CURSOR: + case PLACE_ALL: + case PLACE_ONE: + case PLACE_SOME: + case PICKUP_ALL: + case PICKUP_HALF: + case PICKUP_ONE: + case PICKUP_SOME: + case COLLECT_TO_CURSOR: + return true; + default: + return false; + } + } + + private void scheduleCheck(Player player, boolean sendMessage, long delay) { + if (player == null) { + return; + } + + Bukkit.getScheduler().runTaskLater(MyItems.inst(), () -> { + if (!player.isOnline()) { + return; + } + + boolean changed = suitManager.refreshPlayerSuitState(player, sendMessage); + if (!changed) { + return; + } + + attributeManager.markDirty(player); + attributeManager.refresh(player); + }, delay); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/listener/ToolDurabilityListener.java b/src/main/java/com/io/yaohun/myitems/listener/ToolDurabilityListener.java new file mode 100644 index 0000000..b5da9cb --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/listener/ToolDurabilityListener.java @@ -0,0 +1,77 @@ +package com.io.yaohun.myitems.listener; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.manage.AttributeManager; +import com.io.yaohun.myitems.util.ItemAttributeUtil; +import com.io.yaohun.myitems.util.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; + +public class ToolDurabilityListener implements Listener { + + private final AttributeManager attributeManager; + + public ToolDurabilityListener(AttributeManager attributeManager) { + this.attributeManager = attributeManager; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBreak(BlockBreakEvent event) { + Player player = event.getPlayer(); + + ItemStack item = player.getInventory().getItemInMainHand(); + if (item == null) { + return; + } + + if (!isTool(item.getType())) { + return; + } + + if (!ItemAttributeUtil.hasDurability(item)) { + return; + } + + boolean wasBroken = ItemAttributeUtil.isBroken(item); + + if (wasBroken) { + event.setCancelled(true); + return; + } + + ItemAttributeUtil.damageDurability(item, 1); + + boolean isBroken = ItemAttributeUtil.isBroken(item); + + if (!wasBroken && isBroken) { + MessageUtil.sendMessageKey(player, + "Broken_MainHand", + Sound.ITEM_SHIELD_BREAK); + } + + refreshLater(player); + } + + private boolean isTool(Material type) { + String name = type.name(); + + return name.endsWith("_PICKAXE") + || name.endsWith("_AXE"); + } + + private void refreshLater(Player player) { + attributeManager.markDirty(player); + Bukkit.getScheduler().runTaskLater(MyItems.inst(), () -> { + if (player.isOnline()) { + attributeManager.refresh(player); + } + }, 1L); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/AttributeManager.java b/src/main/java/com/io/yaohun/myitems/manage/AttributeManager.java new file mode 100644 index 0000000..f45523f --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/AttributeManager.java @@ -0,0 +1,269 @@ +package com.io.yaohun.myitems.manage; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.model.PlayerAttributeProfile; +import com.io.yaohun.myitems.util.EquipmentTypeUtil; +import com.io.yaohun.myitems.util.HealthUtil; +import com.io.yaohun.myitems.util.ItemAttributeUtil; +import com.io.yaohun.myitems.util.SpeedUtil; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class AttributeManager { + + private final CacheManager cacheManager; + private final SuitManager suitManager; + + public AttributeManager(CacheManager cacheManager, SuitManager suitManager) { + this.cacheManager = cacheManager; + this.suitManager = suitManager; + } + /** + * 计算玩家最终属性 + */ + public AttributeData calculateFinalAttributes(Player player) { + AttributeData result = new AttributeData(); + + AttributeData base = getBaseAttributes(player); + AttributeData equipment = getEquipmentAttributes(player); + AttributeData suit = getSuitAttributes(player); + AttributeData gems = getGemAttributes(player); + AttributeData buffs = getBuffAttributes(player); + AttributeData temporary = getTemporaryAttributes(player); + + result.merge(base); + result.merge(equipment); + result.merge(suit); + result.merge(gems); + result.merge(buffs); + result.merge(temporary); + applyPveAttackAddition(result); + + return result; + } + + private void applyPveAttackAddition(AttributeData data) { + double pveAttack = data.get(AttributeType.PVE_ATTACK); + double addition = data.get(AttributeType.PVE_ATTACK_ADDITION); + if (Math.abs(pveAttack) <= 0.000001D || Math.abs(addition) <= 0.000001D) { + return; + } + + data.set(AttributeType.PVE_ATTACK, pveAttack * (1.0D + addition / 100.0D)); + } + + /** + * 主动刷新玩家属性缓存 + */ + public void refresh(Player player) { + // MyItems.inst().getLogger().info("已刷新玩家属性: " + player.getName()); + if (player == null || !player.isOnline()) { + return; + } + + AttributeData finalData = calculateFinalAttributes(player); + PlayerAttributeProfile profile = cacheManager.getOrCreate(player.getUniqueId()); + profile.setFinalAttributes(finalData); + profile.setDirty(false); + + HealthUtil.applyMaxHealth(player, finalData); + SpeedUtil.applyMoveSpeed(player, finalData); + //MyItems.inst().getLogger().info("已刷新玩家属性: " + player.getName() + " | main=" + player.getInventory().getItemInMainHand().getType().name()); + } + + /** + * 标记该玩家属性缓存已过期 + */ + public void markDirty(Player player) { + if (player == null) { + return; + } + + PlayerAttributeProfile profile = cacheManager.getOrCreate(player.getUniqueId()); + profile.setDirty(true); + } + + /** + * 获取玩家最终属性 + * 如果缓存是脏的,则先即时刷新一次再返回 + */ + public AttributeData getFinalAttributes(Player player) { + if (player == null) { + return new AttributeData(); + } + + PlayerAttributeProfile profile = cacheManager.getOrCreate(player.getUniqueId()); + + if (profile.isDirty()) { + refresh(player); + profile = cacheManager.getOrCreate(player.getUniqueId()); + } + + return profile.getFinalAttributes(); + } + + /** + * 玩家基础属性 + */ + private AttributeData getBaseAttributes(Player player) { + AttributeData data = new AttributeData(); + + // 这里后续可以接: + // 1. 职业基础属性 + // 2. 等级成长属性 + // 3. 默认初始属性 + + return data; + } + + /** + * 装备属性 + */ + private AttributeData getEquipmentAttributes(Player player) { + AttributeData data = new AttributeData(); + + if (player == null || player.getInventory() == null) { + return data; + } + + mergeIfMatches(data, player.getInventory().getItemInMainHand(), EquipmentSlotType.MAIN_HAND); + mergeIfMatches(data, player.getInventory().getItemInOffHand(), EquipmentSlotType.OFF_HAND); + mergeIfMatches(data, player.getInventory().getHelmet(), EquipmentSlotType.HELMET); + mergeIfMatches(data, player.getInventory().getChestplate(), EquipmentSlotType.CHESTPLATE); + mergeIfMatches(data, player.getInventory().getLeggings(), EquipmentSlotType.LEGGINGS); + mergeIfMatches(data, player.getInventory().getBoots(), EquipmentSlotType.BOOTS); + + return data; + } + + private void mergeIfMatches(AttributeData target, ItemStack item, EquipmentSlotType slotType) { + if (EquipmentTypeUtil.matchesSlot(item, slotType)) { + target.merge(ItemAttributeUtil.readAttributes(item)); + } + } + + private AttributeData getSuitAttributes(Player player) { + if (suitManager == null) { + return new AttributeData(); + } + + return suitManager.getSuitAttributes(player); + } + + /** + * 宝石属性 + */ + private AttributeData getGemAttributes(Player player) { + AttributeData data = new AttributeData(); + + // 后续接宝石系统 + + return data; + } + + /** + * Buff属性 + */ + private AttributeData getBuffAttributes(Player player) { + AttributeData data = new AttributeData(); + + // 后续接药水、称号、区域效果、临时buff + + return data; + } + + /** + * 临时状态属性 + */ + private AttributeData getTemporaryAttributes(Player player) { + AttributeData data = new AttributeData(); + + // 后续接技能触发、短时增伤、短时减伤等 + + return data; + } + + + private AttributeType matchAttributeType(String input) { + if (input == null || input.isEmpty()) { + return null; + } + + switch (input.toLowerCase()) { + case "physical_attack": + case "攻击": + case "物理攻击": + return AttributeType.PHYSICAL_ATTACK; + + case "physical_defense": + case "防御": + case "物理防御": + return AttributeType.PHYSICAL_DEFENSE; + + case "pve_attack": + case "pve攻击": + return AttributeType.PVE_ATTACK; + + case "pve_defense": + case "pve防御": + return AttributeType.PVE_DEFENSE; + + case "max_health_bonus": + case "health": + case "生命": + case "生命增加": + return AttributeType.MAX_HEALTH_BONUS; + + case "crit_chance": + case "暴击": + case "暴击率": + case "暴击概率": + return AttributeType.CRIT_CHANCE; + + case "crit_damage": + case "暴击伤害": + return AttributeType.CRIT_DAMAGE; + + case "hit_chance": + case "命中": + case "命中率": + case "命中几率": + return AttributeType.HIT_CHANCE; + + case "dodge_chance": + case "闪避": + case "闪避率": + case "闪避几率": + return AttributeType.DODGE_CHANCE; + + case "block_chance": + case "block": + case "格挡": + case "格挡概率": + return AttributeType.BLOCK_CHANCE; + + case "vampire_chance": + case "vampire": + case "吸血": + case "吸血概率": + return AttributeType.VAMPIRE_CHANCE; + + case "penetration": + case "护甲穿透": + case "穿透": + return AttributeType.PENETRATION; + + case "move_speed": + case "speed": + case "移速": + case "移动速度": + return AttributeType.MOVE_SPEED; + + default: + return null; + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/CacheManager.java b/src/main/java/com/io/yaohun/myitems/manage/CacheManager.java new file mode 100644 index 0000000..e9f08cd --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/CacheManager.java @@ -0,0 +1,28 @@ +package com.io.yaohun.myitems.manage; + +import com.io.yaohun.myitems.model.PlayerAttributeProfile; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class CacheManager { + + private final Map cacheMap = new ConcurrentHashMap<>(); + + public PlayerAttributeProfile getOrCreate(UUID uuid) { + return cacheMap.computeIfAbsent(uuid, PlayerAttributeProfile::new); + } + + public PlayerAttributeProfile get(UUID uuid) { + return cacheMap.get(uuid); + } + + public void remove(UUID uuid) { + cacheMap.remove(uuid); + } + + public void clear() { + cacheMap.clear(); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/EntityAttributeStorage.java b/src/main/java/com/io/yaohun/myitems/manage/EntityAttributeStorage.java new file mode 100644 index 0000000..2dff121 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/EntityAttributeStorage.java @@ -0,0 +1,78 @@ +package com.io.yaohun.myitems.manage; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class EntityAttributeStorage { + /** + * entityUUID -> (source -> AttributeData) + */ + private final Map> entityAttributes = new ConcurrentHashMap<>(); + + public void addAttributes(UUID uuid, String source, AttributeData data) { + if (uuid == null || source == null || source.isEmpty() || data == null) { + return; + } + + entityAttributes + .computeIfAbsent(uuid, k -> new ConcurrentHashMap<>()) + .put(source, data); + } + + public void removeAttributes(UUID uuid, String source) { + if (uuid == null || source == null || source.isEmpty()) { + return; + } + + Map sourceMap = entityAttributes.get(uuid); + if (sourceMap == null) { + return; + } + + sourceMap.remove(source); + + if (sourceMap.isEmpty()) { + entityAttributes.remove(uuid); + } + } + + public void clearEntity(UUID uuid) { + if (uuid == null) { + return; + } + + entityAttributes.remove(uuid); + } + + public AttributeData getFinalAttributes(UUID uuid) { + AttributeData result = new AttributeData(); + + if (uuid == null) { + return result; + } + + Map sourceMap = entityAttributes.get(uuid); + if (sourceMap == null || sourceMap.isEmpty()) { + return result; + } + + for (AttributeData data : sourceMap.values()) { + result.merge(data); + } + + return result; + } + + public double getAttribute(UUID uuid, AttributeType type) { + return getFinalAttributes(uuid).get(type); + } + + public boolean hasAnyAttribute(UUID uuid) { + Map sourceMap = entityAttributes.get(uuid); + return sourceMap != null && !sourceMap.isEmpty(); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/EquipmentAttributeManager.java b/src/main/java/com/io/yaohun/myitems/manage/EquipmentAttributeManager.java new file mode 100644 index 0000000..73b81db --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/EquipmentAttributeManager.java @@ -0,0 +1,4 @@ +package com.io.yaohun.myitems.manage; + +public class EquipmentAttributeManager { +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/ItemManager.java b/src/main/java/com/io/yaohun/myitems/manage/ItemManager.java new file mode 100644 index 0000000..9d832db --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/ItemManager.java @@ -0,0 +1,192 @@ +package com.io.yaohun.myitems.manage; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.api.TierType; +import com.io.yaohun.myitems.data.CEngineData; +import com.io.yaohun.myitems.data.ItemTemplate; +import com.io.yaohun.myitems.util.DemonAPI; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ItemManager { + + private static final Map templateMap = new HashMap<>(); + + public static ItemTemplate getItemTemplate(String itemId){ + return templateMap.get(itemId); + } + + public static List getTemplateList() { + return new ArrayList<>(templateMap.keySet()); + } + + public static void reloadItemManager(MyItems plugin){ + String folderPath = plugin.getDataFolder()+"/Item_Generator"; + File folder = new File(folderPath); + if (!folder.exists() || !folder.isDirectory()) { + templateMap.clear(); + return; + } + + List ymlFiles = new ArrayList<>(); + DemonAPI.collectYmlFiles(folder, ymlFiles); + if(ymlFiles.isEmpty()){ + templateMap.clear(); + return; + } + + Map loadedTemplates = new HashMap<>(); + for (File file : ymlFiles){ + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + for (String itemId : config.getKeys(false)){ + try { + ItemTemplate template = loadTemplate(config, itemId); + loadedTemplates.put(itemId,template); + } catch (RuntimeException e) { + Bukkit.getLogger().warning("[MyItems] 物品模板加载失败: " + file.getName() + " -> " + itemId + ",原因: " + e.getMessage()); + } + } + } + + templateMap.clear(); + templateMap.putAll(loadedTemplates); + Bukkit.getConsoleSender().sendMessage("§6[Myitems] §7载入物品: §f"+templateMap.size()+"件"); + } + + private static ItemTemplate loadTemplate(FileConfiguration config, String itemId) { + ItemStack itemStack = null; + + Material material = parseMaterial(config, itemId); + String displayName = config.getString(itemId + ".Display_Name", itemId); + List lore = config.getStringList(itemId + ".Lore"); + + String itemIcon = config.getString(itemId+".Item_Icon","ꖐ"); + + String ceNamespace = config.getString(itemId+".CraftEngine.Namespace", "error"); + if(!"error".equalsIgnoreCase(ceNamespace)) { + String ceValue = config.getString(itemId + ".CraftEngine.Value", "default"); + CEngineData engineData = new CEngineData(ceNamespace, ceValue); + if (engineData.isItemStack()) { + itemStack = engineData.getItemStack(); + } + } + + EquipmentSlotType equipSlotType = parseEquipSlot(config, itemId); + Map tierTypes = new HashMap<>(); + Map tierAttributes = new HashMap<>(); + ConfigurationSection section = config.getConfigurationSection(itemId+".Tier_Types"); + if(section == null) { + throw new IllegalArgumentException("缺少 Tier_Types 节点"); + } + + for (String tier : section.getKeys(false)) { + TierType tierType = parseTierType(tier); + double chance = section.getDouble(tier+".Chance"); + if (chance <= 0) { + throw new IllegalArgumentException("品质 " + tier + " 的 Chance 必须大于 0"); + } + tierTypes.put(tierType,chance); + + int currentDurability = section.getInt(tier+".Current_Durability",0); + int maxDurability = section.getInt(tier+".Max_Durability",0); + if (currentDurability < 0 || maxDurability < 0) { + throw new IllegalArgumentException("品质 " + tier + " 的耐久不能为负数"); + } + + Map attributes = new HashMap<>(); + ConfigurationSection attSection = section.getConfigurationSection(tier+".Attributes"); + if(attSection != null) { + for (String att : attSection.getKeys(false)){ + AttributeType attributeType = parseAttributeType(att); + attributes.put(attributeType, parseAttributeValue(attSection.getString(att), tier, att)); + } + } + + ItemTemplate.TierAttribute tierAttribute = new ItemTemplate.TierAttribute(currentDurability, maxDurability, attributes); + tierAttributes.put(tierType,tierAttribute); + } + + if (tierTypes.isEmpty()) { + throw new IllegalArgumentException("至少需要一个有效品质"); + } + + String suitId = config.getString(itemId + ".Suit_Id", "default"); + + return new ItemTemplate(itemId,itemIcon,itemStack, + material,displayName, + lore,equipSlotType, + tierTypes,tierAttributes,suitId); + } + + private static Material parseMaterial(FileConfiguration config, String itemId) { + String materialName = config.getString(itemId + ".Material"); + if (materialName == null || materialName.isEmpty()) { + throw new IllegalArgumentException("缺少 Material"); + } + try { + return Material.valueOf(materialName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效 Material: " + materialName); + } + } + + private static EquipmentSlotType parseEquipSlot(FileConfiguration config, String itemId) { + String slotName = config.getString(itemId+".Equip_Slot"); + if (slotName == null || slotName.isEmpty()) { + throw new IllegalArgumentException("缺少 Equip_Slot"); + } + try { + return EquipmentSlotType.valueOf(slotName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效 Equip_Slot: " + slotName); + } + } + + private static TierType parseTierType(String tier) { + try { + return TierType.valueOf(tier.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效品质类型: " + tier); + } + } + + private static AttributeType parseAttributeType(String attribute) { + try { + return AttributeType.valueOf(attribute.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效属性类型: " + attribute); + } + } + + private static ItemTemplate.AttributeRandomValue parseAttributeValue(String rawValue, String tier, String attribute) { + if (rawValue == null || rawValue.isEmpty()) { + throw new IllegalArgumentException("品质 " + tier + " 的属性 " + attribute + " 缺少数值"); + } + + String[] values = rawValue.split("~"); + if (values.length > 2) { + throw new IllegalArgumentException("品质 " + tier + " 的属性 " + attribute + " 数值格式错误"); + } + + try { + double first = Double.parseDouble(values[0].trim()); + double second = values.length == 1 ? first : Double.parseDouble(values[1].trim()); + return new ItemTemplate.AttributeRandomValue(Math.min(first, second), Math.max(first, second)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("品质 " + tier + " 的属性 " + attribute + " 数值不是数字"); + } + } + +} diff --git a/src/main/java/com/io/yaohun/myitems/manage/SuitManager.java b/src/main/java/com/io/yaohun/myitems/manage/SuitManager.java new file mode 100644 index 0000000..e9ea047 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/manage/SuitManager.java @@ -0,0 +1,269 @@ +package com.io.yaohun.myitems.manage; + +import com.io.yaohun.myitems.MyItems; +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.api.EquipmentSlotType; +import com.io.yaohun.myitems.data.SuitTemplate; +import com.io.yaohun.myitems.util.DemonAPI; +import com.io.yaohun.myitems.util.EquipmentTypeUtil; +import com.io.yaohun.myitems.util.ItemAttributeUtil; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class SuitManager { + + private final Map suitTemplateMap = new HashMap<>(); + private final Map itemSuitMap = new HashMap<>(); + private final Map> playerActiveSuitMap = new HashMap<>(); + + public void reloadSuitManager(MyItems plugin) { + String folderPath = plugin.getDataFolder() + "/Suit_Generator"; + File folder = new File(folderPath); + if (!folder.exists() && !folder.mkdirs()) { + Bukkit.getLogger().warning("[MyItems] 套装配置目录创建失败: " + folderPath); + return; + } + + List ymlFiles = new ArrayList<>(); + DemonAPI.collectYmlFiles(folder, ymlFiles); + if (ymlFiles.isEmpty()) { + suitTemplateMap.clear(); + itemSuitMap.clear(); + playerActiveSuitMap.clear(); + return; + } + + Map loadedSuits = new HashMap<>(); + Map loadedItemSuitMap = new HashMap<>(); + for (File file : ymlFiles) { + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + for (String suitId : config.getKeys(false)) { + try { + SuitTemplate template = loadTemplate(config, suitId, loadedItemSuitMap); + loadedSuits.put(suitId, template); + } catch (RuntimeException e) { + Bukkit.getLogger().warning("[MyItems] 套装模板加载失败: " + file.getName() + " -> " + suitId + ",原因: " + e.getMessage()); + } + } + } + + suitTemplateMap.clear(); + suitTemplateMap.putAll(loadedSuits); + itemSuitMap.clear(); + itemSuitMap.putAll(loadedItemSuitMap); + playerActiveSuitMap.clear(); + Bukkit.getConsoleSender().sendMessage("§6[Myitems] §7载入套装: §f" + suitTemplateMap.size() + "套"); + } + + public AttributeData getSuitAttributes(Player player) { + AttributeData data = new AttributeData(); + if (player == null || player.getInventory() == null || suitTemplateMap.isEmpty()) { + return data; + } + + for (String suitId : getActiveSuitIds(player)) { + SuitTemplate template = suitTemplateMap.get(suitId); + if (template == null) { + continue; + } + + for (Map.Entry attribute : template.getAttributes().entrySet()) { + data.add(attribute.getKey(), attribute.getValue()); + } + } + + return data; + } + + public boolean refreshPlayerSuitState(Player player, boolean sendMessage) { + if (player == null) { + return false; + } + + Set oldSuits = playerActiveSuitMap.getOrDefault(player.getUniqueId(), Collections.emptySet()); + Set newSuits = getActiveSuitIds(player); + if (oldSuits.equals(newSuits)) { + return false; + } + + Set enabled = new HashSet<>(newSuits); + enabled.removeAll(oldSuits); + Set disabled = new HashSet<>(oldSuits); + disabled.removeAll(newSuits); + + playerActiveSuitMap.put(player.getUniqueId(), new HashSet<>(newSuits)); + + if (sendMessage) { + for (String suitId : enabled) { + player.sendMessage(ChatColor.GREEN + "[MyItems] 套装属性已生效: " + ChatColor.WHITE + getDisplayName(suitId)); + } + for (String suitId : disabled) { + player.sendMessage(ChatColor.RED + "[MyItems] 套装属性已失效: " + ChatColor.WHITE + getDisplayName(suitId)); + } + } + + return true; + } + + public Set getActiveSuitIds(Player player) { + if (player == null || player.getInventory() == null || suitTemplateMap.isEmpty()) { + return Collections.emptySet(); + } + + Map suitCounts = getSuitCounts(player); + Set activeSuits = new HashSet<>(); + for (Map.Entry entry : suitCounts.entrySet()) { + SuitTemplate template = suitTemplateMap.get(entry.getKey()); + if (template != null && entry.getValue() >= template.getRequireAmount()) { + activeSuits.add(entry.getKey()); + } + } + return activeSuits; + } + + public void clearPlayerState(Player player) { + if (player != null) { + playerActiveSuitMap.remove(player.getUniqueId()); + } + } + + private String getDisplayName(String suitId) { + SuitTemplate template = suitTemplateMap.get(suitId); + return template == null ? suitId : template.getDisplayName(); + } + + private Map getSuitCounts(Player player) { + Map suitCounts = new HashMap<>(); + Set countedItems = new HashSet<>(); + + countIfMatches(suitCounts, countedItems, player.getInventory().getItemInMainHand(), EquipmentSlotType.MAIN_HAND); + countIfMatches(suitCounts, countedItems, player.getInventory().getItemInOffHand(), EquipmentSlotType.OFF_HAND); + countIfMatches(suitCounts, countedItems, player.getInventory().getHelmet(), EquipmentSlotType.HELMET); + countIfMatches(suitCounts, countedItems, player.getInventory().getChestplate(), EquipmentSlotType.CHESTPLATE); + countIfMatches(suitCounts, countedItems, player.getInventory().getLeggings(), EquipmentSlotType.LEGGINGS); + countIfMatches(suitCounts, countedItems, player.getInventory().getBoots(), EquipmentSlotType.BOOTS); + + return suitCounts; + } + + private void countIfMatches(Map suitCounts, Set countedItems, + ItemStack item, EquipmentSlotType slotType) { + if (!EquipmentTypeUtil.matchesSlot(item, slotType) || ItemAttributeUtil.isBroken(item)) { + return; + } + + String itemId = ItemAttributeUtil.readTemplateId(item); + if (itemId == null || itemId.isEmpty()) { + return; + } + + String suitId = ItemAttributeUtil.readSuitId(item); + if (suitId == null || suitId.isEmpty()) { + suitId = itemSuitMap.get(itemId); + } + + SuitTemplate template = suitTemplateMap.get(suitId); + if (template == null || !template.containsItem(itemId)) { + return; + } + + String countedKey = suitId + ":" + itemId; + if (!countedItems.add(countedKey)) { + return; + } + + suitCounts.put(suitId, suitCounts.getOrDefault(suitId, 0) + 1); + } + + private SuitTemplate loadTemplate(FileConfiguration config, String suitId, Map loadedItemSuitMap) { + String displayName = config.getString(suitId + ".Display_Name", suitId); + int requireAmount = config.getInt(suitId + ".Require", config.getInt(suitId + ".需求件数", 4)); + if (requireAmount <= 0) { + throw new IllegalArgumentException("Require 必须大于 0"); + } + + List itemList = config.getStringList(suitId + ".Items"); + if (itemList.isEmpty()) { + itemList = config.getStringList(suitId + ".物品"); + } + if (itemList.isEmpty()) { + throw new IllegalArgumentException("缺少 Items 或 物品"); + } + + Set itemIds = new HashSet<>(itemList); + for (String itemId : itemIds) { + String oldSuitId = loadedItemSuitMap.get(itemId); + if (oldSuitId != null && !oldSuitId.equals(suitId)) { + throw new IllegalArgumentException("物品 " + itemId + " 同时属于 " + oldSuitId + " 与 " + suitId); + } + } + for (String itemId : itemIds) { + loadedItemSuitMap.put(itemId, suitId); + } + + Map attributes = loadAttributes(config, suitId); + if (attributes.isEmpty()) { + throw new IllegalArgumentException("缺少 Attributes 或 属性"); + } + + return new SuitTemplate(suitId, displayName, requireAmount, itemIds, attributes); + } + + private Map loadAttributes(FileConfiguration config, String suitId) { + ConfigurationSection section = config.getConfigurationSection(suitId + ".Attributes"); + if (section == null) { + section = config.getConfigurationSection(suitId + ".属性"); + } + + Map attributes = new HashMap<>(); + if (section == null) { + return attributes; + } + + for (String key : section.getKeys(false)) { + AttributeType type = parseAttributeType(key); + attributes.put(type, parseAttributeValue(section.getString(key), key)); + } + return attributes; + } + + private AttributeType parseAttributeType(String attribute) { + try { + return AttributeType.valueOf(attribute.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效属性类型: " + attribute); + } + } + + private double parseAttributeValue(String rawValue, String attribute) { + if (rawValue == null || rawValue.isEmpty()) { + throw new IllegalArgumentException("属性 " + attribute + " 缺少数值"); + } + + if (rawValue.contains("~")) { + throw new IllegalArgumentException("属性 " + attribute + " 必须使用固定数值"); + } + + try { + return Double.parseDouble(rawValue.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("属性 " + attribute + " 数值不是数字"); + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/model/PlayerAttributeProfile.java b/src/main/java/com/io/yaohun/myitems/model/PlayerAttributeProfile.java new file mode 100644 index 0000000..543af13 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/model/PlayerAttributeProfile.java @@ -0,0 +1,45 @@ +package com.io.yaohun.myitems.model; + +import com.io.yaohun.myitems.api.AttributeData; + +import java.util.UUID; + +public class PlayerAttributeProfile { + + private final UUID uuid; + private AttributeData finalAttributes; + private long lastUpdateTime; + private boolean dirty; + + public PlayerAttributeProfile(UUID uuid) { + this.uuid = uuid; + this.finalAttributes = new AttributeData(); + this.lastUpdateTime = System.currentTimeMillis(); + this.dirty = true; + } + + public UUID getUuid() { + return uuid; + } + + public AttributeData getFinalAttributes() { + return finalAttributes; + } + + public void setFinalAttributes(AttributeData finalAttributes) { + this.finalAttributes = finalAttributes; + this.lastUpdateTime = System.currentTimeMillis(); + } + + public long getLastUpdateTime() { + return lastUpdateTime; + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean dirty) { + this.dirty = dirty; + } +} \ No newline at end of file diff --git a/src/main/java/com/io/yaohun/myitems/util/AttributeFormatUtil.java b/src/main/java/com/io/yaohun/myitems/util/AttributeFormatUtil.java new file mode 100644 index 0000000..0b1804d --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/AttributeFormatUtil.java @@ -0,0 +1,54 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.AttributeType; + +import java.text.DecimalFormat; + +public final class AttributeFormatUtil { + + private static final DecimalFormat INT_FORMAT = new DecimalFormat("0"); + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("0.##"); + + public static String format(AttributeType type, double value) { + switch (type) { + case PHYSICAL_ATTACK: + case PHYSICAL_DEFENSE: + case PVE_ATTACK: + case PVE_DEFENSE: + case MAX_HEALTH_BONUS: + case EQUIPMENT_DURABILITY: + return INT_FORMAT.format(value); + + case CRIT_CHANCE: + case CRIT_DAMAGE: + case HIT_CHANCE: + case DODGE_CHANCE: + case BLOCK_CHANCE: + case VAMPIRE_CHANCE: + case PENETRATION: + case MOVE_SPEED: + case PVE_ATTACK_ADDITION: + return DOUBLE_FORMAT.format(value) + "%"; + + default: + return DOUBLE_FORMAT.format(value); + } + } + + public static boolean isPercent(AttributeType type) { + switch (type) { + case CRIT_CHANCE: + case CRIT_DAMAGE: + case HIT_CHANCE: + case DODGE_CHANCE: + case BLOCK_CHANCE: + case VAMPIRE_CHANCE: + case PENETRATION: + case MOVE_SPEED: + case PVE_ATTACK_ADDITION: + return true; + default: + return false; + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/AttributeKeys.java b/src/main/java/com/io/yaohun/myitems/util/AttributeKeys.java new file mode 100644 index 0000000..9b5b6c6 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/AttributeKeys.java @@ -0,0 +1,54 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.MyItems; +import org.bukkit.NamespacedKey; + +public final class AttributeKeys { + public static final NamespacedKey EQUIP_SLOT = + new NamespacedKey(MyItems.inst(), "equip_slot"); + public static final NamespacedKey TEMPLATE_ID = + new NamespacedKey(MyItems.inst(), "template_id"); + public static final NamespacedKey SUIT_ID = + new NamespacedKey(MyItems.inst(), "suit_id"); + + public static final NamespacedKey PHYSICAL_ATTACK = + new NamespacedKey(MyItems.inst(), "physical_attack"); + public static final NamespacedKey PHYSICAL_DEFENSE = + new NamespacedKey(MyItems.inst(), "physical_defense"); + + public static final NamespacedKey PVE_ATTACK = + new NamespacedKey(MyItems.inst(), "pve_attack"); + public static final NamespacedKey PVE_ATTACK_ADDITION = + new NamespacedKey(MyItems.inst(), "pve_attack_addition"); + public static final NamespacedKey PVE_DEFENSE = + new NamespacedKey(MyItems.inst(), "pve_defense"); + + public static final NamespacedKey MAX_HEALTH_BONUS = + new NamespacedKey(MyItems.inst(), "max_health_bonus"); + + public static final NamespacedKey CRIT_CHANCE = + new NamespacedKey(MyItems.inst(), "crit_chance"); + public static final NamespacedKey CRIT_DAMAGE = + new NamespacedKey(MyItems.inst(), "crit_damage"); + + public static final NamespacedKey HIT_CHANCE = + new NamespacedKey(MyItems.inst(), "hit_chance"); + public static final NamespacedKey DODGE_CHANCE = + new NamespacedKey(MyItems.inst(), "dodge_chance"); + public static final NamespacedKey BLOCK_CHANCE = + new NamespacedKey(MyItems.inst(), "block_chance"); + public static final NamespacedKey VAMPIRE_CHANCE = + new NamespacedKey(MyItems.inst(), "vampire_chance"); + + public static final NamespacedKey CURRENT_DURABILITY = + new NamespacedKey(MyItems.inst(), "current_durability"); + public static final NamespacedKey MAX_DURABILITY = + new NamespacedKey(MyItems.inst(), "max_durability"); + + + public static final NamespacedKey PENETRATION = + new NamespacedKey(MyItems.inst(), "penetration"); + + public static final NamespacedKey MOVE_SPEED = + new NamespacedKey(MyItems.inst(), "move_speed"); +} diff --git a/src/main/java/com/io/yaohun/myitems/util/DemonAPI.java b/src/main/java/com/io/yaohun/myitems/util/DemonAPI.java new file mode 100644 index 0000000..c4f0a6f --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/DemonAPI.java @@ -0,0 +1,58 @@ +package com.io.yaohun.myitems.util; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DemonAPI { + + public static boolean itemIsNull(ItemStack item){ + return item == null || item.getType() == Material.AIR; + } + + public static boolean itemIsLore(ItemStack item) { + if (item == null || !item.hasItemMeta()) { + return true; + } + List lore = item.getItemMeta().getLore(); + return lore == null || lore.isEmpty(); + } + + public static void collectYmlFiles(File dir, List result) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + collectYmlFiles(file, result); // 递归子目录 + } else if (file.isFile() && file.getName().endsWith(".yml")) { + result.add(file); + } + } + } + + public static String convertHexColor(String text) { + Pattern pattern = Pattern.compile("#[a-fA-F0-9]{6}"); + Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + String hexCode = text.substring(matcher.start(), matcher.end()); + String replaceSharp = hexCode.replace('#', 'x'); + + char[] ch = replaceSharp.toCharArray(); + StringBuilder builder = new StringBuilder(); + for (char c : ch) { + builder.append("&").append(c); + } + + text = text.replace(hexCode, builder.toString()); + matcher = pattern.matcher(text); + } + return ChatColor.translateAlternateColorCodes('&', text); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/EquipmentCheckUtil.java b/src/main/java/com/io/yaohun/myitems/util/EquipmentCheckUtil.java new file mode 100644 index 0000000..02e4e54 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/EquipmentCheckUtil.java @@ -0,0 +1,46 @@ +package com.io.yaohun.myitems.util; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class EquipmentCheckUtil { + + public static boolean isValidItem(ItemStack item) { + return item != null && item.getType() != Material.AIR; + } + + public static boolean isBrokenWeapon(Player player) { + if (player == null) { + return false; + } + + ItemStack item = player.getInventory().getItemInMainHand(); + return isBrokenEquipment(item); + } + + public static boolean isBrokenEquipment(ItemStack item) { + if (!isValidItem(item)) { + return false; + } + + return ItemAttributeUtil.hasDurability(item) && ItemAttributeUtil.isBroken(item); + } + + public static boolean hasCustomAttributes(ItemStack item) { + if (!isValidItem(item)) { + return false; + } + + return ItemAttributeUtil.hasAnyAttribute(item); + } + + public static boolean isBrokenCustomWeapon(Player player) { + if (player == null) { + return false; + } + + ItemStack item = player.getInventory().getItemInMainHand(); + return hasCustomAttributes(item) && isBrokenEquipment(item); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/EquipmentTypeUtil.java b/src/main/java/com/io/yaohun/myitems/util/EquipmentTypeUtil.java new file mode 100644 index 0000000..7f509f6 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/EquipmentTypeUtil.java @@ -0,0 +1,80 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.EquipmentSlotType; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +public class EquipmentTypeUtil { + + public static boolean matchesSlot(ItemStack item, EquipmentSlotType slotType) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + + if (meta != null) { + String customSlot = meta.getPersistentDataContainer().get( + AttributeKeys.EQUIP_SLOT, + PersistentDataType.STRING + ); + + if (customSlot != null && !customSlot.isEmpty()) { + + if ("UNIVERSAL".equalsIgnoreCase(customSlot)) { + return true; + } + + return customSlot.equalsIgnoreCase(slotType.name()); + } + } + return matchesVanillaMaterial(item, slotType); + } + + public static boolean matchesVanillaMaterial(ItemStack item, EquipmentSlotType slotType) { + if (item == null || item.getType() == Material.AIR) { + return false; + } + + Material type = item.getType(); + String name = type.name(); + + switch (slotType) { + case MAIN_HAND, OFF_HAND: + return isWeapon(item); + + case HELMET: + return name.endsWith("_HELMET") + || name.equals("TURTLE_HELMET"); + + case CHESTPLATE: + return name.endsWith("_CHESTPLATE"); + + case LEGGINGS: + return name.endsWith("_LEGGINGS"); + + case BOOTS: + return name.endsWith("_BOOTS"); + + default: + return false; + } + } + + public static boolean isWeapon(ItemStack item) { + if (item == null || item.getType() == Material.AIR) { + return false; + } + + String name = item.getType().name(); + + return name.endsWith("_SWORD") + || name.endsWith("_AXE") + || name.equals("BOW") + || name.equals("CROSSBOW") + || name.equals("TRIDENT"); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/HealthUtil.java b/src/main/java/com/io/yaohun/myitems/util/HealthUtil.java new file mode 100644 index 0000000..6a5576f --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/HealthUtil.java @@ -0,0 +1,33 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Player; + +public final class HealthUtil { + + private HealthUtil() { + } + + public static void applyMaxHealth(Player player, AttributeData data) { + if (player == null || data == null) return; + + AttributeInstance instance = player.getAttribute(Attribute.MAX_HEALTH); + if (instance == null) return; + + double bonusHealth = data.get(AttributeType.MAX_HEALTH_BONUS); + double finalHealth = 20.0 + bonusHealth; + + if (finalHealth < 1.0) { + finalHealth = 1.0; + } + + instance.setBaseValue(finalHealth); + + if (player.getHealth() > finalHealth) { + player.setHealth(finalHealth); + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/ItemAttributeEditor.java b/src/main/java/com/io/yaohun/myitems/util/ItemAttributeEditor.java new file mode 100644 index 0000000..bc2feeb --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/ItemAttributeEditor.java @@ -0,0 +1,241 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.builder.LoreBuilder; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Map; + +public class ItemAttributeEditor { + + public static ItemStack setAttribute(ItemStack item, AttributeType type, double value) { + if (item == null || item.getType() == Material.AIR) { + return item; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + NamespacedKey key = getKey(type); + + if (key == null) { + return item; + } + + if (Math.abs(value) <= 0.000001D) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.DOUBLE, value); + } + + item.setItemMeta(meta); + LoreBuilder.updateLore(item); + return item; + } + + public static ItemStack addAttribute(ItemStack item, AttributeType type, double addValue) { + double oldValue = ItemAttributeUtil.readSingleAttribute(item, type); + return setAttribute(item, type, oldValue + addValue); + } + + public static ItemStack removeAttribute(ItemStack item, AttributeType type) { + return setAttribute(item, type, 0.0D); + } + + public static ItemStack setAttributes(ItemStack item, Map attributeMap) { + if (item == null || attributeMap == null || attributeMap.isEmpty()) { + return item; + } + + for (Map.Entry entry : attributeMap.entrySet()) { + setAttribute(item, entry.getKey(), entry.getValue()); + } + + LoreBuilder.updateLore(item); + return item; + } + + public static ItemStack clearAllAttributes(ItemStack item) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return item; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + removeIfExists(pdc, AttributeKeys.PHYSICAL_ATTACK); + removeIfExists(pdc, AttributeKeys.PHYSICAL_DEFENSE); + removeIfExists(pdc, AttributeKeys.PVE_ATTACK); + removeIfExists(pdc, AttributeKeys.PVE_ATTACK_ADDITION); + removeIfExists(pdc, AttributeKeys.PVE_DEFENSE); + removeIfExists(pdc, AttributeKeys.MAX_HEALTH_BONUS); + removeIfExists(pdc, AttributeKeys.CRIT_CHANCE); + removeIfExists(pdc, AttributeKeys.CRIT_DAMAGE); + removeIfExists(pdc, AttributeKeys.HIT_CHANCE); + removeIfExists(pdc, AttributeKeys.DODGE_CHANCE); + removeIfExists(pdc, AttributeKeys.BLOCK_CHANCE); + removeIfExists(pdc, AttributeKeys.VAMPIRE_CHANCE); + removeIfExists(pdc, AttributeKeys.PENETRATION); + removeIfExists(pdc, AttributeKeys.MOVE_SPEED); + + item.setItemMeta(meta); + LoreBuilder.updateLore(item); + return item; + } + + public static ItemStack setDurability(ItemStack item, int current, int max) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return item; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + if (max <= 0) { + pdc.remove(AttributeKeys.CURRENT_DURABILITY); + pdc.remove(AttributeKeys.MAX_DURABILITY); + } else { + int fixedMax = Math.max(1, max); + int fixedCurrent = Math.max(0, Math.min(current, fixedMax)); + + pdc.set(AttributeKeys.CURRENT_DURABILITY, PersistentDataType.INTEGER, fixedCurrent); + pdc.set(AttributeKeys.MAX_DURABILITY, PersistentDataType.INTEGER, fixedMax); + } + + item.setItemMeta(meta); + // LoreBuilder.updateLore(item); + return item; + } + + public static ItemStack damageDurability(ItemStack item, int damage) { + if (item == null || damage <= 0) { + return item; + } + + int max = ItemAttributeUtil.getMaxDurability(item); + if (max <= 0) { + return item; + } + + int current = ItemAttributeUtil.getCurrentDurability(item); + return setDurability(item, current - damage, max); + } + + public static ItemStack repairDurability(ItemStack item, int amount) { + if (item == null || amount <= 0) { + return item; + } + + int max = ItemAttributeUtil.getMaxDurability(item); + if (max <= 0) { + return item; + } + + int current = ItemAttributeUtil.getCurrentDurability(item); + return setDurability(item, current + amount, max); + } + + public static ItemStack setEquipSlot(ItemStack item, String slotName) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return item; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + meta.getPersistentDataContainer().set( + AttributeKeys.EQUIP_SLOT, + PersistentDataType.STRING, + slotName + ); + + item.setItemMeta(meta); + return item; + } + + public static ItemStack setTemplateId(ItemStack item, String templateId) { + return setStringData(item, AttributeKeys.TEMPLATE_ID, templateId); + } + + public static ItemStack setSuitId(ItemStack item, String suitId) { + return setStringData(item, AttributeKeys.SUIT_ID, suitId); + } + + private static NamespacedKey getKey(AttributeType type) { + switch (type) { + case PHYSICAL_ATTACK: + return AttributeKeys.PHYSICAL_ATTACK; + case PHYSICAL_DEFENSE: + return AttributeKeys.PHYSICAL_DEFENSE; + case PVE_ATTACK: + return AttributeKeys.PVE_ATTACK; + case PVE_ATTACK_ADDITION: + return AttributeKeys.PVE_ATTACK_ADDITION; + case PVE_DEFENSE: + return AttributeKeys.PVE_DEFENSE; + case MAX_HEALTH_BONUS: + return AttributeKeys.MAX_HEALTH_BONUS; + case CRIT_CHANCE: + return AttributeKeys.CRIT_CHANCE; + case CRIT_DAMAGE: + return AttributeKeys.CRIT_DAMAGE; + case HIT_CHANCE: + return AttributeKeys.HIT_CHANCE; + case DODGE_CHANCE: + return AttributeKeys.DODGE_CHANCE; + case BLOCK_CHANCE: + return AttributeKeys.BLOCK_CHANCE; + case VAMPIRE_CHANCE: + return AttributeKeys.VAMPIRE_CHANCE; + case PENETRATION: + return AttributeKeys.PENETRATION; + case MOVE_SPEED: + return AttributeKeys.MOVE_SPEED; + default: + return null; + } + } + + private static void removeIfExists(PersistentDataContainer pdc, NamespacedKey key) { + pdc.remove(key); + } + + private static ItemStack setStringData(ItemStack item, NamespacedKey key, String value) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return item; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + if (value == null || value.isEmpty()) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, value); + } + + item.setItemMeta(meta); + return item; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/ItemAttributeUtil.java b/src/main/java/com/io/yaohun/myitems/util/ItemAttributeUtil.java new file mode 100644 index 0000000..11c98b6 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/ItemAttributeUtil.java @@ -0,0 +1,298 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import com.io.yaohun.myitems.builder.LoreBuilder; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public final class ItemAttributeUtil { + + /** + * 读取整件装备属性 + * 若装备已破损,则不提供任何属性 + */ + public static AttributeData readAttributes(ItemStack item) { + AttributeData data = new AttributeData(); + + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return data; + } + // 破损装备属性失效 + if (isBroken(item)) { + return data; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return data; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + data.add(AttributeType.PHYSICAL_ATTACK, getDouble(pdc, AttributeKeys.PHYSICAL_ATTACK)); + data.add(AttributeType.PHYSICAL_DEFENSE, getDouble(pdc, AttributeKeys.PHYSICAL_DEFENSE)); + + data.add(AttributeType.PVE_ATTACK, getDouble(pdc, AttributeKeys.PVE_ATTACK)); + data.add(AttributeType.PVE_ATTACK_ADDITION, getDouble(pdc, AttributeKeys.PVE_ATTACK_ADDITION)); + data.add(AttributeType.PVE_DEFENSE, getDouble(pdc, AttributeKeys.PVE_DEFENSE)); + + data.add(AttributeType.MAX_HEALTH_BONUS, getDouble(pdc, AttributeKeys.MAX_HEALTH_BONUS)); + + data.add(AttributeType.CRIT_CHANCE, getDouble(pdc, AttributeKeys.CRIT_CHANCE)); + data.add(AttributeType.CRIT_DAMAGE, getDouble(pdc, AttributeKeys.CRIT_DAMAGE)); + + data.add(AttributeType.HIT_CHANCE, getDouble(pdc, AttributeKeys.HIT_CHANCE)); + data.add(AttributeType.DODGE_CHANCE, getDouble(pdc, AttributeKeys.DODGE_CHANCE)); + data.add(AttributeType.BLOCK_CHANCE, getDouble(pdc, AttributeKeys.BLOCK_CHANCE)); + data.add(AttributeType.VAMPIRE_CHANCE, getDouble(pdc, AttributeKeys.VAMPIRE_CHANCE)); + + data.add(AttributeType.PENETRATION, getDouble(pdc, AttributeKeys.PENETRATION)); + data.add(AttributeType.MOVE_SPEED, getDouble(pdc, AttributeKeys.MOVE_SPEED)); + + return data; + } + + /** + * 读取单个属性 + * 若装备已破损,则属性视为 0 + */ + public static double readSingleAttribute(ItemStack item, AttributeType type) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return 0.0D; + } + + if (isBroken(item)) { + return 0.0D; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return 0.0D; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + switch (type) { + case PHYSICAL_ATTACK: + return getDouble(pdc, AttributeKeys.PHYSICAL_ATTACK); + case PHYSICAL_DEFENSE: + return getDouble(pdc, AttributeKeys.PHYSICAL_DEFENSE); + case PVE_ATTACK: + return getDouble(pdc, AttributeKeys.PVE_ATTACK); + case PVE_ATTACK_ADDITION: + return getDouble(pdc, AttributeKeys.PVE_ATTACK_ADDITION); + case PVE_DEFENSE: + return getDouble(pdc, AttributeKeys.PVE_DEFENSE); + case MAX_HEALTH_BONUS: + return getDouble(pdc, AttributeKeys.MAX_HEALTH_BONUS); + case CRIT_CHANCE: + return getDouble(pdc, AttributeKeys.CRIT_CHANCE); + case CRIT_DAMAGE: + return getDouble(pdc, AttributeKeys.CRIT_DAMAGE); + case HIT_CHANCE: + return getDouble(pdc, AttributeKeys.HIT_CHANCE); + case DODGE_CHANCE: + return getDouble(pdc, AttributeKeys.DODGE_CHANCE); + case BLOCK_CHANCE: + return getDouble(pdc, AttributeKeys.BLOCK_CHANCE); + case VAMPIRE_CHANCE: + return getDouble(pdc, AttributeKeys.VAMPIRE_CHANCE); + case PENETRATION: + return getDouble(pdc, AttributeKeys.PENETRATION); + case MOVE_SPEED: + return getDouble(pdc, AttributeKeys.MOVE_SPEED); + case EQUIPMENT_DURABILITY: + return getMaxDurability(item); + default: + return 0.0D; + } + } + + public static boolean hasDurability(ItemStack item) { + return getMaxDurability(item) > 0; + } + + public static int getCurrentDurability(ItemStack item) { + if (item == null || !item.hasItemMeta()) return 0; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return 0; + return meta.getPersistentDataContainer().getOrDefault( + AttributeKeys.CURRENT_DURABILITY, + PersistentDataType.INTEGER, + 0 + ); + } + + public static int getMaxDurability(ItemStack item) { + if (item == null || !item.hasItemMeta()) return 0; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return 0; + return meta.getPersistentDataContainer().getOrDefault( + AttributeKeys.MAX_DURABILITY, + PersistentDataType.INTEGER, + 0 + ); + } + + public static void setCurrentDurability(ItemStack item, int value) { + if (item == null || !item.hasItemMeta()) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + + meta.getPersistentDataContainer().set( + AttributeKeys.CURRENT_DURABILITY, + PersistentDataType.INTEGER, + Math.max(0, value) + ); + item.setItemMeta(meta); + } + + public static void setMaxDurability(ItemStack item, int value) { + if (item == null || !item.hasItemMeta()) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + + meta.getPersistentDataContainer().set( + AttributeKeys.MAX_DURABILITY, + PersistentDataType.INTEGER, + Math.max(0, value) + ); + item.setItemMeta(meta); + } + + public static void setDurability(ItemStack item, int current, int max) { + if(DemonAPI.itemIsNull(item) || DemonAPI.itemIsLore(item)){ + return; + } + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + if (max <= 0) { + pdc.remove(AttributeKeys.CURRENT_DURABILITY); + pdc.remove(AttributeKeys.MAX_DURABILITY); + } else { + int fixedMax = Math.max(1, max); + int fixedCurrent = Math.max(0, Math.min(current, fixedMax)); + + pdc.set(AttributeKeys.CURRENT_DURABILITY, PersistentDataType.INTEGER, fixedCurrent); + pdc.set(AttributeKeys.MAX_DURABILITY, PersistentDataType.INTEGER, fixedMax); + } + + item.setItemMeta(meta); + + LoreBuilder.updateDurabilityAttributeLine(item); + } + + public static void damageDurability(ItemStack item, int damage) { + if (item == null || damage <= 0) { + return; + } + + int max = getMaxDurability(item); + if (max <= 0) { + return; + } + + int current = getCurrentDurability(item); + setDurability(item, current - damage, max); + } + + public static void repairDurability(ItemStack item, int amount) { + if (item == null || amount <= 0) { + return; + } + + int max = getMaxDurability(item); + if (max <= 0) { + return; + } + + int current = getCurrentDurability(item); + setDurability(item, current + amount, max); + } + + public static boolean isBroken(ItemStack item) { + int max = getMaxDurability(item); + if (max <= 0) { + return false; + } + + int current = getCurrentDurability(item); + return current <= 0; + } + + public static String readTemplateId(ItemStack item) { + return readString(item, AttributeKeys.TEMPLATE_ID); + } + + public static String readSuitId(ItemStack item) { + return readString(item, AttributeKeys.SUIT_ID); + } + + public static boolean hasAnyAttribute(ItemStack item) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return false; + } + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + return has(pdc, AttributeKeys.PHYSICAL_ATTACK) + || has(pdc, AttributeKeys.PHYSICAL_DEFENSE) + || has(pdc, AttributeKeys.PVE_ATTACK) + || has(pdc, AttributeKeys.PVE_ATTACK_ADDITION) + || has(pdc, AttributeKeys.PVE_DEFENSE) + || has(pdc, AttributeKeys.MAX_HEALTH_BONUS) + || has(pdc, AttributeKeys.CRIT_CHANCE) + || has(pdc, AttributeKeys.CRIT_DAMAGE) + || has(pdc, AttributeKeys.HIT_CHANCE) + || has(pdc, AttributeKeys.DODGE_CHANCE) + || has(pdc, AttributeKeys.BLOCK_CHANCE) + || has(pdc, AttributeKeys.VAMPIRE_CHANCE) + || has(pdc, AttributeKeys.PENETRATION) + || has(pdc, AttributeKeys.MOVE_SPEED); + } + + private static boolean has(PersistentDataContainer pdc, NamespacedKey key) { + return pdc.has(key, PersistentDataType.DOUBLE) || pdc.has(key, PersistentDataType.INTEGER); + } + + private static double getDouble(PersistentDataContainer pdc, NamespacedKey key) { + if (pdc.has(key, PersistentDataType.DOUBLE)) { + return pdc.getOrDefault(key, PersistentDataType.DOUBLE, 0.0D); + } + + if (pdc.has(key, PersistentDataType.INTEGER)) { + return pdc.getOrDefault(key, PersistentDataType.INTEGER, 0); + } + + return 0.0D; + } + + private static String readString(ItemStack item, NamespacedKey key) { + if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { + return ""; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return ""; + } + + return meta.getPersistentDataContainer().getOrDefault( + key, + PersistentDataType.STRING, + "" + ); + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/MessageUtil.java b/src/main/java/com/io/yaohun/myitems/util/MessageUtil.java new file mode 100644 index 0000000..d758aae --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/MessageUtil.java @@ -0,0 +1,71 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.MyItems; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import java.io.File; +import java.util.HashMap; + +public class MessageUtil { + + private static HashMap messageMap = new HashMap<>(); + + public static void init(MyItems plugin) { + messageMap.clear(); + File file = new File(plugin.getDataFolder(),"lang.yml"); + // 如果没有这个文件就新建一个~ + if(!file.exists()){ + plugin.saveResource("lang.yml",false); + } + FileConfiguration yml = YamlConfiguration.loadConfiguration(file); + for (String ymlKey : yml.getKeys(false)){ + ConfigurationSection section = yml.getConfigurationSection(ymlKey); + for (String key : section.getKeys(false)){ + String message = DemonAPI.convertHexColor(section.getString(key)); + messageMap.put(key,message.replace("&","§")); + } + } + } + + public static String getMessage(String key){ + if(messageMap.containsKey(key)){ + return messageMap.get(key); + } + return "§c未找到节点.§7#"+key; + } + + public static void broadcast(String msg) { + Bukkit.getServer().broadcastMessage( msg.replace("&","§")); + } + + public static void sendMessage(CommandSender sender, String message){ + sender.sendMessage(message); + } + + public static void sendMessageKey(CommandSender sender, String key, Sound sound){ + sender.sendMessage(getMessage(key)); + if(sender instanceof Player player){ + player.playSound(player.getLocation(),sound,1,1); + } + } + + public static void sendMessage(CommandSender sender, String message, Sound sound){ + sender.sendMessage(message); + if(sender instanceof Player player){ + player.playSound(player.getLocation(),sound,1,1); + } + } + + public static void sendDescMessage(CommandSender sender, String message, Sound sound){ + sender.sendMessage("§f[§c消息§f] §a正确用法: §e"+message); + if(sender instanceof Player player){ + player.playSound(player.getLocation(),sound,1,1); + } + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/RandomBuilder.java b/src/main/java/com/io/yaohun/myitems/util/RandomBuilder.java new file mode 100644 index 0000000..a3c57a7 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/RandomBuilder.java @@ -0,0 +1,83 @@ +package com.io.yaohun.myitems.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class RandomBuilder { + + private Map valueMap; + + private List elementList = new ArrayList<>(); + private double maxElement = 0; + + public RandomBuilder(Map map) { + valueMap = map; + + double minElement = 0; + + for (Map.Entry entry : map.entrySet()) { + minElement = maxElement; + maxElement += entry.getValue(); + elementList.add(new Element(entry.getKey(), minElement, maxElement)); + } + } + + public T random() { + return random(new Random()); + } + + public T random(Random random) { + double d = random.nextDouble() * maxElement; + if (d == 0) { + d = 0.1 * maxElement; + } + int size = valueMap.size(); + for (int i = 0; i < elementList.size(); i++) { + Element element = elementList.get(i); + if (element.isContainKey(d)) { + return element.getValue(); + } + } + return null; + } + + public double getMaxElement() { + return maxElement; + } + + public List getElementList() { + return elementList; + } + + public Map getValueMap() { + return valueMap; + } + + private class Element { + + private T value; + + private final double minElement; + private final double maxElement; + + public Element(T value, double element0, double element1) { + this.value = value; + this.minElement = Math.min(element0, element1); + this.maxElement = Math.max(element0, element1); + } + + public T getValue() { + return value; + } + + public boolean isContainKey(double element) { + return element > minElement && element <= maxElement; + } + + } + + + +} diff --git a/src/main/java/com/io/yaohun/myitems/util/RandomUtil.java b/src/main/java/com/io/yaohun/myitems/util/RandomUtil.java new file mode 100644 index 0000000..29b5b89 --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/RandomUtil.java @@ -0,0 +1,74 @@ +package com.io.yaohun.myitems.util; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Random; + +public class RandomUtil { + + public static double round(double d, int scale) { + return new BigDecimal(Double.toString(d)).setScale(scale, RoundingMode.HALF_UP).doubleValue(); + } + public static boolean random(double d) { + return d >= new Random().nextFloat(); + } + + public static int getRandomInt(int min, int max) { + if (min == max) { + return max; + } + Random r = new Random(); + int i = min < max ? min : max; + int a = min < max ? max : min; + return r.nextInt(a - i + 1) + i; + } + + public static double getRandomDouble(double min, double max, int scl) { + int pow = (int) Math.pow(10, scl); + return Math.floor((Math.random() * (max - min) + min) * pow) / pow; + } + + public static Location getRandomLocation(World world, double minX, double maxX, double minY, double maxY, double minZ, double maxZ) { + double rx = getRandomDouble(minX, maxX, 2); + double ry = getRandomDouble(minY, maxY, 2); + double rz = getRandomDouble(minZ, maxZ, 2); + return new Location(world, rx, ry, rz); + } + + public static Location getRandomLocation(Player player, double minX, double maxX, double minY, double maxY, double minZ, double maxZ) { + World world = player.getWorld(); + Location playerLocation = player.getLocation(); + Location location = playerLocation.add(getRandomInt(-3,3),0,getRandomInt(-3,3)); + for (int i = 0; i < 10;i++) { + Location loc = new Location(world, getRandomDouble(minX, maxX, 3), getRandomDouble(minY, maxY, 3), getRandomDouble(minZ, maxZ, 3)); + if (loc.distance(playerLocation) <= 50) { + location = loc; + break; + } + } + return location; + } + + public static Location getPlayerRandomLocation(Player player) { + Location location = player.getLocation().clone(); + double rx = RandomUtil.getRandomDouble(-3, 3, 3); + double ry = RandomUtil.getRandomDouble(0, 2, 3); + double rz = RandomUtil.getRandomDouble(-3, 3, 3); + location.add(rx, ry, rz); + return location; + } + + public static Location getPlayerRandomLocation(Player player,int range) { + Location location = player.getLocation().clone(); + double rx = RandomUtil.getRandomDouble(-range, range, 3); + double ry = RandomUtil.getRandomDouble(0, 2, 3); + double rz = RandomUtil.getRandomDouble(-range, range, 3); + location.add(rx, ry, rz); + return location; + } + +} diff --git a/src/main/java/com/io/yaohun/myitems/util/RepairUtil.java b/src/main/java/com/io/yaohun/myitems/util/RepairUtil.java new file mode 100644 index 0000000..809a48b --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/RepairUtil.java @@ -0,0 +1,37 @@ +package com.io.yaohun.myitems.util; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class RepairUtil { + public static boolean repairToFull(ItemStack item) { + if (item == null) { + return false; + } + + int max = ItemAttributeUtil.getMaxDurability(item); + if (max <= 0) { + return false; + } + + ItemAttributeUtil.setDurability(item, max, max); + return true; + } + + public static int repairAll(Player player) { + if (player == null) { + return 0; + } + + int repaired = 0; + + if (repairToFull(player.getInventory().getItemInMainHand())) repaired++; + if (repairToFull(player.getInventory().getItemInOffHand())) repaired++; + if (repairToFull(player.getInventory().getHelmet())) repaired++; + if (repairToFull(player.getInventory().getChestplate())) repaired++; + if (repairToFull(player.getInventory().getLeggings())) repaired++; + if (repairToFull(player.getInventory().getBoots())) repaired++; + + return repaired; + } +} diff --git a/src/main/java/com/io/yaohun/myitems/util/SpeedUtil.java b/src/main/java/com/io/yaohun/myitems/util/SpeedUtil.java new file mode 100644 index 0000000..bc4000a --- /dev/null +++ b/src/main/java/com/io/yaohun/myitems/util/SpeedUtil.java @@ -0,0 +1,37 @@ +package com.io.yaohun.myitems.util; + +import com.io.yaohun.myitems.api.AttributeData; +import com.io.yaohun.myitems.api.AttributeType; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Player; + +public class SpeedUtil { + + public static void applyMoveSpeed(Player player, AttributeData data) { + if (player == null || data == null) return; + + AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (instance == null) return; + + double bonusPercent = data.get(AttributeType.MOVE_SPEED); + + // 原版默认移动速度大约是 0.1 + double base = 0.1D; + double finalSpeed = base * (1.0D + bonusPercent / 100.0D); + + // 防止异常速度 + finalSpeed = Math.max(0.01D, Math.min(finalSpeed, 0.5D)); + + instance.setBaseValue(finalSpeed); + } + + public static void reset(Player player) { + if (player == null) return; + + AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (instance == null) return; + + instance.setBaseValue(0.1D); + } +} diff --git a/src/main/resources/Item_Generator/稀有工具箱.yml b/src/main/resources/Item_Generator/稀有工具箱.yml new file mode 100644 index 0000000..f66ad2b --- /dev/null +++ b/src/main/resources/Item_Generator/稀有工具箱.yml @@ -0,0 +1,15 @@ +xygjx: + ItemStack: default + Material: DIAMOND_SWORD + Display_Name: "§7[{tier_prefix}{tier_name}§7] §f铁镐" + Lore: + - "§7耐久§f§l: §c{now}§7/§a{max}" + - "§a✔§f从稀有工具箱I获得" + Equip_Slot: MAIN_HAND + Tier_Types: + UNCOMMON: + Chance: 10.0 + Current_Durability: 40 + Max_Durability: 40 + Attributes: + PHYSICAL_ATTACK: 100 diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..1982d3c --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,7 @@ +hub-spawn: + world: "world" + x: 8.5 + y: 61.0 + z: 8.5 + yaw: 0.0 + pitch: 0.0 \ No newline at end of file diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml new file mode 100644 index 0000000..fd07dbe --- /dev/null +++ b/src/main/resources/lang.yml @@ -0,0 +1,40 @@ +Stats: + PHYSICAL_ATTACK: " #CF0000ꖋ 攻击伤害 +" + PHYSICAL_DEFENSE: " #FF9F00ꖋ 物理防御 +" + PVE_ATTACK: " #8ba2baꖋ PvE伤害 +" + PVE_DEFENSE: " #8ba2baꖋ PvE护甲 +" + MAX_HEALTH_BONUS: " #00B836ꖋ 生命增加 +" + CRIT_CHANCE: " #8ba2baꖋ 暴击概率 +" + CRIT_DAMAGE: " #8ba2baꖋ 暴击伤害 +" + HIT_CHANCE: " #8ba2baꖋ 命中概率 +" + DODGE_CHANCE: " #8ba2baꖋ 闪避概率 +" + BLOCK_CHANCE: " #8ba2baꖋ 格挡概率 +" + VAMPIRE_CHANCE: " #CF0000ꖋ 吸血概率 +" + PENETRATION: " #8ba2baꖋ 护甲穿透 +" + MOVE_SPEED: " #00B836ꖋ 移速加成 +" + EQUIPMENT_DURABILITY: " #A3A3A3ꖋ 耐久" + Att_Int_Format: "0" + Att_Double_Format: "0.##" + +MyItems: + Stats_Success: "§f[§6属性§f] §7属性&c{stats}&7已被修改为: &e{value}" + Slot_Success: "§f[§6属性§f] §7手持物品的槽位已经修改为: &e{value}" + Durability_Success: "§f[§6属性§f] §7当前物品耐久已设置为: &c{now}&7/&e{max}" + Clear_Success: "§f[§6属性§f] §a成功删除手中物品所有属性." + Repair_Success: "§f[§6战争领域§f] §a你手中物品耐久已修复." + Repair_All_Success: "§f[§6战争领域§f] §a背包中所有物品耐久已修复." +Item: + MainHand_Empty: "§f[§6属性§f] §c你的主手没有物品." + Broken_MainHand: "&7[&6战争领域&7] &f你手上的物品耐久值已用尽,请到主城修复,否则无法使用!" + Broken_Offhand: "&7[&6战争领域&7] &f你副手的物品耐久值已用尽,请到主城修复,否则无法使用!" + Broken_Helmet: "&7[&6战争领域&7] &f你的帽子已经穿烂了!" + Broken_Chestplate: "&7[&6战争领域&7] &f你的衣服已经穿烂了!" + Broken_Leggings: "&7[&6战争领域&7] &f你的裤子已经穿烂了!" + Broken_Boots: "&7[&6战争领域&7] &f你的鞋子已经穿烂了!" + Durability_Not_Exist: "§f[§6战争领域§f] §c该物品不支持耐久修复." + Durability_Not_Amount: "§f[§6战争领域§f] §c背包中没有物品可修复." +Argument: + Invalid_Value: "§f[§6属性§f] §c无效的数值." + Invalid_SlotType: "§f[§6属性§f] §c无效的槽位类型." + + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..3bfe165 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,12 @@ +name: AuMyItems +main: com.io.yaohun.myitems.MyItems +version: 1.1.3 +api-version: 1.21 +authors: [YaoHunYa] +depend: + - CraftEngine +commands: + att: + repair: + myitems: + aliases: [mi] diff --git a/使用案例.md b/使用案例.md new file mode 100644 index 0000000..b948689 --- /dev/null +++ b/使用案例.md @@ -0,0 +1,347 @@ +# AuQuestEngine + +AuQuestEngine 是一个面向 Paper / Spigot 服务端的任务系统插件,用于通过配置文件定义任务、任务目标、接取条件、完成提示和奖励命令。 + +当前插件版本:`1.0.6` + +## 1. 功能概览 + +- 支持每日、每周、每月、长期任务。 +- 支持 YAML、SQLite、MySQL 三种玩家任务数据存储方式。 +- 支持任务接取条件:玩家等级、前置已完成任务、指定世界。 +- 支持多种 Bukkit 事件任务目标,例如破坏方块、放置方块、合成、拾取、击杀、钓鱼、村民交易、打开菜单等。 +- 支持 PlaceholderAPI 变量。 +- 支持 Citizens、MythicMobs、CustomFishing、CustomCrops 等前置插件的扩展任务监听。 +- 支持任务完成后执行控制台奖励命令。 + +## 2. 运行环境 + +```text +Java: 21 +服务端: Paper 1.21.11 +构建工具: Maven +``` + +前置插件: + +```yml +softdepend: + - Citizens + - MythicMobs + - PlaceholderAPI + - CustomFishing + - CustomCrops +``` + +这些前置不是全部必装。未安装对应插件时,相关任务监听器不会注册。 + +## 3. 安装方式 + +构建插件: + +```bash +mvn package +``` + +构建完成后,将生成的插件包放入服务端: + +```text +plugins/AuQuestEngine-1.0-SNAPSHOT.jar +``` + +启动服务端后,插件会生成基础配置文件。任务文件需要放在: + +```text +plugins/AuQuestEngine/Quests/ +``` + +## 4. 配置文件 + +`config.yml` 用于配置存储方式和部分任务类型识别列表。 + +```yml +Storage: + Type: yaml # yaml / sqlite / mysql + SQLite: + File: PlayerData.db + SaveInterval: 10 + MySQL: + Host: localhost + Port: 3306 + Database: quest + Username: root + Password: root + TablePrefix: AuQuest_ + Pool: + MaximumPoolSize: 10 + MinimumIdle: 2 + ConnectionTimeout: 10000 + IdleTimeout: 600000 + MaxLifetime: 1800000 +``` + +存储说明: + +- `yaml`:玩家数据保存为单独 YAML 文件,适合小型服务器。 +- `sqlite`:玩家数据保存到本地 SQLite 数据库。 +- `mysql`:玩家数据保存到 MySQL,适合多实例或长期运营环境。 + +插件包已包含 SQLite JDBC、MySQL Connector/J 和 HikariCP。 + +## 5. 任务文件格式 + +任务文件可以放在 `plugins/AuQuestEngine/Quests/` 目录下,文件名不限,后缀必须是 `.yml`。 + +示例: + +```yml +quest_1: + display_name: "&a[初级] 新手成长之路" + description: + - "&7完成基础生存挑战任务" + - "&7砍伐指定木材,熟悉资源收集" + type: DAILY + messages: + receive: + - "&a你接受了任务:&f新手成长之路" + complete: + - "&a任务完成:&f新手成长之路" + - "&7奖励已发放,请继续成长。" + conditions: + level: 0 + completed_quests: [] + worlds: [] + tasks: + id_1: + type: BREAK_BLOCK + display_name: "砍伐云杉木 * 128" + amount: 128 + targets: + - SPRUCE_LOG + - SPRUCE_WOOD + - STRIPPED_SPRUCE_LOG + - STRIPPED_SPRUCE_WOOD + rewards: + commands: + - "dlevel give %player% 12000" + - "points give %player% 5" + messages: + - "&a{name} 任务已完成" +``` + +字段说明: + +- `display_name`:任务显示名称。 +- `description`:任务描述。 +- `type`:任务类型,可用 `DAILY`、`WEEKLY`、`MONTHLY`、`FIXED`。 +- `messages.receive`:接取任务后发送给玩家的消息。 +- `messages.complete`:完成任务后发送给玩家的消息。 +- `conditions.level`:最低等级要求。 +- `conditions.completed_quests`:前置已完成任务 ID 列表。 +- `conditions.worlds`:允许推进任务的世界列表,留空表示不限制。 +- `tasks`:任务目标列表。 +- `rewards.commands`:完成后由控制台执行的命令,使用 `%player%` 替换玩家名。 +- `rewards.messages`:奖励提示,支持 `{name}` 替换任务名称。 + +## 6. 任务类型 + +常用任务类型: + +```text +BREAK_BLOCK 破坏方块 +PLACE_BLOCK 放置方块 +CRAFT_ITEM 合成物品 +PICKUP_ITEM 拾取物品 +FISH_ITEM 钓鱼获得物品 +FISH_STAR 钓起鱼的星级 +PLACE_BUCKET 倒水、倒岩浆、放置粉雪 +FILL_BUCKET 装水、装岩浆、装牛奶 +HARVEST_CROP 收获农作物 +EXP_GAIN 获取经验 +BREED 繁殖动物 +COLLECT_ENTITY 剪羊毛、取蘑菇煲 +COLLECT_BLOCK 收集蜂蜜 +TAME_ENTITY 驯服实体 +VILLAGER_TRADE_ITEM 村民交易物品 +VILLAGER_TRADE_JOB 村民职业交易 +BREW_POTION 酿造药水 +BREW_POTION_COUNT 酿造药水次数 +USE_ANVIL 使用铁砧 +ENCHANT_ITEM 附魔物品 +FEED_ANIMAL 喂动物 +FEED_PLAYER 玩家食用物品 +SMELT_ORE 烧制矿物 +SMELT_FOOD 烧制食物 +INTERACT_NPC 交互 Citizens NPC +INTERACT_ENTITY 交互实体 +INTERACT_ITEM 交互手持物品 +SEND_COMMAND 发送命令 +SEND_CHAT 发送聊天 +OPEN_GUI_TITLE 打开指定标题菜单 +OPEN_GUI_TYPE 打开指定类型菜单 +KILL_TYPE 击杀原版实体 +KILL_MYTHIC 击杀 MythicMobs 实体 +``` + +预留类型存在于枚举中,但未必已有完整监听实现。配置任务前应确认对应监听器已实现。 + +## 7. 玩家命令 + +查看任务信息: + +```text +/aquest info +``` + +管理员查看指定在线玩家任务信息: + +```text +/aquest info <玩家名> +``` + +给玩家发放任务: + +```text +/aquest js <任务ID> <玩家名> +``` + +OP 玩家直接完成自身任务: + +```text +/aquest direc <任务ID> +``` + +## 8. 管理命令 + +管理命令需要权限: + +```text +admin.use +``` + +重载配置、语言和任务: + +```text +/aquestadmin reload +``` + +开关调试输出: + +```text +/aquestadmin debug +``` + +清空指定玩家任务数据: + +```text +/aquestadmin clear <玩家名> +``` + +SQLite 数据导出为 YAML: + +```text +/aquestadmin outsqlite +``` + +## 9. PlaceholderAPI 变量 + +插件标识: + +```text +aquest +``` + +任务名称: + +```text +%aquest_quest_<任务ID>_name% +``` + +任务是否完成: + +```text +%aquest_quest_<任务ID>_completed% +``` + +任务总进度: + +```text +%aquest_quest_<任务ID>_progress% +``` + +指定任务目标进度: + +```text +%aquest_task_<任务ID>_<任务目标ID>% +``` + +示例: + +```text +%aquest_quest_quest_1_name% +%aquest_quest_quest_1_progress% +%aquest_task_quest_1_id_1% +``` + +## 10. API 调用 + +其他插件可以通过 `AuQuestAPI` 查询或操作玩家任务。 + +```java +UUID uuid = player.getUniqueId(); + +boolean started = AuQuestAPI.startQuest(uuid, "quest_1"); +boolean completed = AuQuestAPI.hasCompletedQuest(uuid, "quest_1"); +boolean active = AuQuestAPI.hasActiveQuest(uuid, "quest_1"); + +List activeQuests = AuQuestAPI.getActiveQuests(uuid); +List completedQuests = AuQuestAPI.getCompletedQuests(uuid); + +AuQuestAPI.directCompleteQuest(uuid, "quest_1"); +AuQuestAPI.abandonQuest(uuid, "quest_1"); +``` + +常用方法: + +- `getQuestDisplayName(String questId)` +- `getQuestDescription(String questId)` +- `getQuestType(String questId)` +- `hasCompletedQuest(UUID uuid, String questId)` +- `hasActiveQuest(UUID uuid, String questId)` +- `getActiveQuests(UUID uuid)` +- `getCompletedQuests(UUID uuid)` +- `startQuest(UUID uuid, String questId)` +- `abandonQuest(UUID uuid, String questId)` +- `directCompleteQuest(UUID uuid, String questId)` + +## 11. 自动刷新规则 + +任务刷新按任务类型处理: + +- `DAILY`:每日 00:00 后刷新。 +- `WEEKLY`:每周一 00:00 后刷新。 +- `MONTHLY`:每月 1 日 00:00 后刷新。 +- `FIXED`:长期任务,不参与周期刷新。 + +刷新时会清理对应类型任务的历史进度,并同步在线玩家缓存。 + +## 12. 使用流程 + +1. 将插件 jar 放入服务端 `plugins` 目录。 +2. 启动服务端生成配置文件。 +3. 根据需要选择 `yaml`、`sqlite` 或 `mysql` 存储。 +4. 在 `plugins/AuQuestEngine/Quests/` 下创建任务配置文件。 +5. 执行 `/aquestadmin reload` 重载任务。 +6. 使用 `/aquest js <任务ID> <玩家名>` 给玩家发放任务。 +7. 玩家完成目标后自动累计进度。 +8. 任务完成后自动发送完成消息并执行奖励命令。 + +## 13. 注意事项 + +- 修改任务文件后需要执行 `/aquestadmin reload`。 +- 数据库模式切换前应先备份旧数据。 +- MySQL 模式需要确认数据库、账号和权限正确。 +- CustomFishing、CustomCrops、Citizens、MythicMobs 未安装时,对应任务类型不会生效。 +- 任务目标 `targets` 必须与监听器传入的目标字符串一致,原版方块和物品通常使用 Bukkit 枚举名。 +- 任务奖励命令由控制台执行,配置前应确认命令不会被重复领取滥用。 +- 发布新版本前应确认 `plugin.yml` 中版本号已递增。 diff --git a/插件投入使用测试流程.md b/插件投入使用测试流程.md new file mode 100644 index 0000000..ee5f981 --- /dev/null +++ b/插件投入使用测试流程.md @@ -0,0 +1,458 @@ +## AuMyItems 投入使用测试流程 + +生成时间:2026-07-08 05:05:00 + +### 1. 测试目标 + +确认插件满足以下条件后,才允许投入使用: +- 插件可完成 Maven 构建。 +- 服务端可正常启动并加载插件。 +- CraftEngine 依赖可被服务端识别。 +- 物品模板可加载。 +- 属性写入、读取、展示、生效链路正常。 +- 穿透、格挡、吸血语义符合当前设计。 +- 耐久扣减、破损失效、修复逻辑正常。 +- reload 不产生异常或旧数据残留。 + +### 2. 测试环境要求 + +- Java:21 +- 服务端:Paper 1.21.11 +- 必装依赖:CraftEngine 26.5.1 +- 插件版本:1.0.7 +- 测试账号:至少 2 个玩家账号 +- 推荐准备: + - 一个测试平坦世界 + - 一个普通怪物生成区 + - 一个 PVP 测试区域 + - 控制台日志实时可见 + +### 3. 构建验证 + +#### 3.1 执行构建 + +命令: +```bash +mvn -q -DskipTests package +``` + +通过标准: +- 命令退出码为 0。 +- 生成插件 jar。 +- 控制台无编译错误。 + +失败处理: +- 如果 CraftEngine 依赖解析失败,先检查 Maven 仓库配置。 +- 如果 Maven 插件下载失败,先处理本机网络或 Maven 缓存。 +- 构建失败不得进入服务端测试。 + +#### 3.2 检查插件描述文件 + +检查 jar 内 `plugin.yml`: +- `version` 必须是 `1.0.7`。 +- `depend` 必须包含 `CraftEngine`。 +- `main` 必须指向主类。 + +通过标准: +- jar 内版本号与发布版本一致。 +- 没有连续构建沿用旧版本号。 + +#### 3.3 检查依赖打包 + +检查 jar 内容: +- 不得包含 `org/sqlite/**` +- 不得包含 `com/zaxxer/hikari/**` +- 不得包含 `net/momirealms/craftengine/**` + +通过标准: +- 上述类路径均不存在。 + +### 4. 服务端启动验证 + +#### 4.1 安装插件 + +将以下插件放入服务端插件目录: +- CraftEngine +- AuMyItems + +启动服务端。 + +通过标准: +- 控制台无红色报错。 +- AuMyItems 正常启用。 +- CraftEngine 在 AuMyItems 前完成加载。 +- 控制台出现物品模板加载数量日志。 + +失败处理: +- 若提示缺少 CraftEngine,检查依赖插件文件名、版本和加载顺序。 +- 若模板加载报错,记录具体物品 ID 和原因。 + +### 5. 配置加载验证 + +#### 5.1 默认配置生成 + +删除测试服中的旧插件数据目录后启动一次。 + +通过标准: +- 默认配置文件生成。 +- 默认语言文件生成。 +- 默认物品模板生成。 +- 无启动异常。 + +#### 5.2 旧配置兼容 + +使用旧版本配置启动。 + +通过标准: +- 缺失新配置不会导致插件无法启动。 +- 模板错误只跳过单个物品,不影响其他模板加载。 +- 控制台能明确提示错误物品和原因。 + +### 6. 命令验证 + +#### 6.1 权限验证 + +无权限玩家执行: +```text +/att +/repair repair +/myitems reload +``` + +通过标准: +- 无权限玩家不能执行管理功能。 +- 控制台无异常。 + +#### 6.2 属性写入验证 + +管理员手持普通物品,依次执行: +```text +/att stats PHYSICAL_ATTACK 100 +/att stats PHYSICAL_DEFENSE 50 +/att stats PVE_ATTACK 30 +/att stats PVE_DEFENSE 20 +/att stats MAX_HEALTH_BONUS 40 +/att stats CRIT_CHANCE 100 +/att stats CRIT_DAMAGE 100 +/att stats HIT_CHANCE 100 +/att stats DODGE_CHANCE 100 +/att stats PENETRATION 100 +/att stats BLOCK_CHANCE 100 +/att stats VAMPIRE_CHANCE 100 +/att stats MOVE_SPEED 50 +``` + +通过标准: +- 每条命令返回成功提示。 +- 物品 Lore 显示对应属性。 +- 不出现“未找到节点”文案。 +- 控制台无异常。 + +#### 6.3 槽位写入验证 + +执行: +```text +/att slot MAIN_HAND +/att slot OFF_HAND +/att slot HELMET +/att slot CHESTPLATE +/att slot LEGGINGS +/att slot BOOTS +``` + +通过标准: +- 每个合法槽位都能写入。 +- 物品放入对应槽位时属性生效。 +- 放入不匹配槽位时属性不生效。 + +#### 6.4 耐久写入验证 + +执行: +```text +/att durability 10 10 +``` + +通过标准: +- Lore 显示耐久。 +- 当前耐久和最大耐久一致。 +- 无异常。 + +### 7. 物品模板验证 + +#### 7.1 重载模板 + +执行: +```text +/myitems reload +``` + +通过标准: +- 控制台显示模板加载数量。 +- 坏模板只跳过自身。 +- 好模板仍可生成。 + +#### 7.2 生成模板物品 + +执行: +```text +/myitems give xygjx +``` + +通过标准: +- 玩家获得物品。 +- 名称、Lore、品质、属性、耐久均正确。 +- 单值属性配置可正常解析。 +- 区间属性配置可正常随机。 + +### 8. 属性面板验证 + +执行: +```text +/myitems gui +``` + +通过标准: +- GUI 可打开。 +- 攻击属性显示: + - 物理攻击 + - PVE 攻击 + - 暴击概率 + - 暴击伤害 + - 命中几率 + - 护甲穿透 + - 吸血几率 +- 防御属性显示: + - 物理防御 + - PVE 防御 + - 生命提升 + - 闪避几率 + - 格挡几率 + - 移速加成 +- 点击 GUI 不会取出展示物品。 + +### 9. 战斗属性验证 + +#### 9.1 基础伤害 + +准备: +- 攻击者主手武器设置 `PHYSICAL_ATTACK 100` +- 目标不穿任何装备 + +操作: +- 攻击目标。 + +通过标准: +- 伤害明显高于无属性武器。 +- ActionBar 显示伤害数字。 + +#### 9.2 防御减伤 + +准备: +- 目标装备设置 `PHYSICAL_DEFENSE 1000` +- 攻击者武器设置 `PHYSICAL_ATTACK 100` + +通过标准: +- 目标受到伤害低于无防御状态。 + +#### 9.3 穿透触发 + +准备: +- 攻击者武器设置 `PENETRATION 100` +- 目标装备设置高额 `PHYSICAL_DEFENSE` + +操作: +- 攻击目标多次。 + +通过标准: +- 每次都应触发穿透。 +- ActionBar 出现穿透提示。 +- 伤害应接近无防御状态,而不是按防御减伤。 + +#### 9.4 格挡触发 + +准备: +- 目标装备设置 `BLOCK_CHANCE 100` +- 攻击者武器设置稳定攻击属性。 + +操作: +- 攻击目标。 + +通过标准: +- 每次都应触发格挡。 +- 目标 ActionBar 出现格挡提示。 +- 目标受到最终伤害降低约 50%。 +- 攻击者受到反伤。 +- 反伤不会造成无限递归触发。 + +#### 9.5 吸血触发 + +准备: +- 攻击者武器设置 `VAMPIRE_CHANCE 100` +- 攻击者先损失生命值。 + +操作: +- 攻击目标。 + +通过标准: +- 每次都应触发吸血。 +- ActionBar 出现吸血提示。 +- 攻击者生命恢复。 +- 恢复量不超过最大生命值。 + +#### 9.6 暴击触发 + +准备: +- 攻击者武器设置 `CRIT_CHANCE 100` +- 攻击者武器设置 `CRIT_DAMAGE 100` + +通过标准: +- 每次攻击都显示暴击。 +- 最终伤害高于非暴击伤害。 + +#### 9.7 闪避触发 + +准备: +- 目标装备设置 `DODGE_CHANCE 100` +- 攻击者不设置额外命中。 + +通过标准: +- 目标应频繁或稳定闪避,具体取决于命中公式钳制。 +- 闪避时事件取消,伤害为 0。 + +#### 9.8 PVE 属性 + +准备: +- 攻击者武器设置 `PVE_ATTACK 100` +- 怪物装备或实体属性设置 `PVE_DEFENSE` + +通过标准: +- 玩家攻击怪物时 PVE 攻击参与计算。 +- 玩家攻击玩家时不应使用 PVE 攻击加成。 + +### 10. 耐久系统验证 + +#### 10.1 攻击扣耐久 + +准备: +- 主手武器设置耐久 `3/3` + +操作: +- 攻击实体 3 次。 + +通过标准: +- 每次攻击后耐久减少。 +- 耐久归零后发送破损提示。 +- 破损后该装备属性失效。 + +#### 10.2 受击扣护甲耐久 + +准备: +- 护甲设置耐久 `3/3` + +操作: +- 让玩家受到伤害 3 次。 + +通过标准: +- 护甲耐久减少。 +- 耐久归零后发送对应部位破损提示。 +- 破损后该装备属性失效。 + +#### 10.3 工具挖掘扣耐久 + +准备: +- 镐或斧设置耐久 `3/3` + +操作: +- 挖掘方块 3 次。 + +通过标准: +- 工具耐久减少。 +- 耐久归零后阻止继续使用。 + +#### 10.4 修复命令 + +执行: +```text +/repair repair +/repair repairall +/repair repair <玩家名> +/repair repairall <玩家名> +``` + +通过标准: +- 目标玩家为空时给出提示,不报错。 +- 指定玩家时修复目标玩家物品。 +- 修复后属性刷新。 + +### 11. 重载与缓存验证 + +操作: +1. 穿戴属性装备。 +2. 打开属性面板记录属性。 +3. 执行 `/myitems reload`。 +4. 切换装备或重新打开面板。 + +通过标准: +- reload 不报错。 +- 已穿戴装备属性不丢失。 +- 新模板配置生效。 +- 玩家属性缓存能正确刷新。 + +### 12. 下线与资源释放验证 + +操作: +1. 玩家穿戴属性装备。 +2. 进入战斗。 +3. 玩家退出服务器。 +4. 玩家重新进入。 + +通过标准: +- 退出时缓存释放。 +- 重新进入后属性重新计算。 +- 移速和生命值不残留异常。 + +### 13. 实体属性 API 验证 + +使用插件内部或临时测试命令给实体写入: +- `physical_defense` +- `pve_defense` +- `block_chance` +- `vampire_chance` +- `penetration` +- `move_speed` + +通过标准: +- 已支持的字符串能映射到正确属性。 +- 实体死亡或区块卸载后属性缓存释放。 + +### 14. 回归检查清单 + +- [ ] 构建通过。 +- [ ] plugin.yml 版本正确。 +- [ ] CraftEngine 已安装且先于本插件加载。 +- [ ] 物品模板加载数量符合预期。 +- [ ] `/att stats` 支持所有 AttributeType。 +- [ ] Lore 不出现缺失节点。 +- [ ] GUI 属性完整显示。 +- [ ] 穿透按概率触发并无视防御。 +- [ ] 格挡按概率触发并减伤反伤。 +- [ ] 吸血按概率触发并回血。 +- [ ] 破损装备属性失效。 +- [ ] 修复命令修复目标正确。 +- [ ] reload 后无异常。 +- [ ] 玩家退出后无属性残留。 +- [ ] jar 未打入 SQLite、HikariCP、CraftEngine 类。 + +### 15. 投入使用判定 + +允许投入使用必须同时满足: +- 构建验证通过。 +- 服务端启动验证通过。 +- 命令验证通过。 +- 属性面板验证通过。 +- 战斗属性验证通过。 +- 耐久系统验证通过。 +- 重载与缓存验证通过。 +- 无高风险控制台异常。 + +若任一核心战斗属性未通过,结论为不允许投入使用。