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. 投入使用判定
+
+允许投入使用必须同时满足:
+- 构建验证通过。
+- 服务端启动验证通过。
+- 命令验证通过。
+- 属性面板验证通过。
+- 战斗属性验证通过。
+- 耐久系统验证通过。
+- 重载与缓存验证通过。
+- 无高风险控制台异常。
+
+若任一核心战斗属性未通过,结论为不允许投入使用。