package cn.infstar.magicgem.gem; import cn.infstar.magicgem.api.event.MagicGemUseEvent; import cn.infstar.magicgem.dsl.Directive; import cn.infstar.magicgem.condition.ConditionEngine; import cn.infstar.magicgem.config.LanguageManager; import cn.infstar.magicgem.expression.DynamicExpression; import cn.infstar.magicgem.item.ItemCodec; import cn.infstar.magicgem.item.ItemMatcher; import cn.infstar.magicgem.reward.RewardContext; import cn.infstar.magicgem.reward.RewardEngine; import cn.infstar.magicgem.reward.RewardException; import org.bukkit.entity.Player; import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; public final class GemService { private final GemRegistry registry; private final ItemCodec itemCodec; private final ItemMatcher matcher; private final RewardEngine rewards; private final DynamicExpression expression; private final ConditionEngine conditions; private final LanguageManager languages; private final Map> cooldowns = new ConcurrentHashMap<>(); public GemService(GemRegistry registry, ItemCodec itemCodec, ItemMatcher matcher, RewardEngine rewards, DynamicExpression expression, ConditionEngine conditions, LanguageManager languages) { this.registry = registry; this.itemCodec = itemCodec; this.matcher = matcher; this.rewards = rewards; this.expression = expression; this.conditions = conditions; this.languages = languages; } public GemUseResult use(Player player, GemDefinition requested, ItemStack target) { MagicGemUseEvent event = new MagicGemUseEvent(player, requested, true); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) return GemUseResult.rejected(target, languages.text("cmagic.use_cancelled", "&c宝石使用已被其他插件取消")); GemDefinition gem = resolveRandom(requested); if (gem == null) return GemUseResult.rejected(target, languages.text("cmagic.random_invalid", "&c随机宝石没有有效目标")); if (requested.type() == GemType.RANDOM_GEM && requested.giveRandomGem()) { give(player, gem, 1); return new GemUseResult(true, true, target, requested.successTip()); } GemUseResult result = switch (gem.type()) { case ITEM_GEM -> useItem(player, requested, gem, target); case PLAYER_GEM -> usePlayer(player, requested, gem, target); case RANDOM_GEM -> GemUseResult.rejected(target, languages.text("cmagic.random_cycle", "&c随机宝石不能循环引用")); }; if (requested.type() != GemType.RANDOM_GEM) return result; return new GemUseResult(true, result.successful(), result.target(), result.message()); } public GemType effectiveType(GemDefinition gem) { return effectiveType(gem, new HashSet<>()); } private GemType effectiveType(GemDefinition gem, Set visiting) { if (gem.type() != GemType.RANDOM_GEM) return gem.type(); if (!visiting.add(gem.name().toLowerCase(java.util.Locale.ROOT))) return GemType.RANDOM_GEM; GemType resolved = null; for (GemDefinition.WeightedGem weighted : gem.randomGems()) { GemDefinition nested = registry.find(weighted.name()).orElse(null); if (nested == null) continue; GemType type = effectiveType(nested, new HashSet<>(visiting)); if (type == GemType.RANDOM_GEM || resolved != null && resolved != type) return GemType.RANDOM_GEM; resolved = type; } return resolved == null ? GemType.RANDOM_GEM : resolved; } private GemUseResult useItem(Player player, GemDefinition consumedGem, GemDefinition gem, ItemStack target) { if (!matcher.matches(target, gem.requirements())) return GemUseResult.rejected(target, languages.text("gem.tip.wrong_type", "&c目标装备不符合宝石要求")); ConditionEngine.CheckResult condition = conditions.check(gem.conditions(), player, target); if (!condition.success()) return GemUseResult.rejected(target, condition.message()); Map embedded = new LinkedHashMap<>(itemCodec.embedded(target)); if (gem.embedLimit() > 0 && embedded.getOrDefault(gem.name(), 0) >= gem.embedLimit()) return GemUseResult.rejected(target, languages.text("gem.tip.embed_limit", "&c已达到该宝石的镶嵌上限")); boolean successful = roll(gem.successExpression(), player, target); RewardContext context = new RewardContext(player, gem, target, false); if (!successful) { try { rewards.applyPlayerRewards(context, gem.rewards(), Directive.Phase.FAIL); } catch (RewardException exception) { return GemUseResult.rejected(target, exception.getMessage()); } if (embedded.getOrDefault(gem.name(), 0) > 0 && roll(gem.downgradeExpression(), player, target)) { RewardContext downgrade = new RewardContext(player, gem, target, true); try { ItemStack changed = rewards.applyItemRewards(downgrade, gem.rewards(), Directive.Phase.REMOVE); embedded.compute(gem.name(), (ignored, value) -> value == null || value <= 1 ? null : value - 1); changed = itemCodec.setEmbedded(changed, embedded); downgrade.item(changed); return new GemUseResult(true, false, changed, defaultMessage(gem.downgradeTip(), languages.text("gem.tip.downgrade", "&c强化失败,装备已降级"))); } catch (RewardException exception) { return GemUseResult.rejected(target, exception.getMessage()); } } return new GemUseResult(true, false, target, defaultMessage(gem.failTip(), languages.text("gem.tip.fail", "&c宝石使用失败"))); } try { ItemStack changed = rewards.applyItemRewards(context, gem.rewards(), Directive.Phase.SUCCESS); context.item(changed); if (gem.embedLimit() > 0) { embedded.merge(gem.name(), 1, Integer::sum); changed = itemCodec.setEmbedded(changed, embedded); context.item(changed); } rewards.applyPlayerRewards(context, gem.rewards(), Directive.Phase.SUCCESS); return new GemUseResult(true, true, context.item(), defaultMessage(gem.successTip(), languages.text("gem.tip.success", "&a宝石使用成功"))); } catch (RewardException | RuntimeException exception) { return GemUseResult.rejected(target, exception.getMessage()); } } private GemUseResult usePlayer(Player player, GemDefinition consumedGem, GemDefinition gem, ItemStack target) { ConditionEngine.CheckResult condition = conditions.check(gem.conditions(), player, target); if (!condition.success()) return GemUseResult.rejected(target, condition.message()); long now = System.currentTimeMillis(); Map playerCooldowns = cooldowns.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>()); long ready = playerCooldowns.getOrDefault(gem.name(), 0L); if (ready > now) return GemUseResult.rejected(target, cooldownTip(gem.cooldownTip(), (ready - now) / 1000.0, gem.cooldownSeconds())); boolean successful = roll(gem.successExpression(), player, target); RewardContext context = new RewardContext(player, gem, target, false); try { rewards.applyPlayerRewards(context, gem.rewards(), successful ? Directive.Phase.SUCCESS : Directive.Phase.FAIL); if (gem.cooldownSeconds() > 0) playerCooldowns.put(gem.name(), now + (long) (gem.cooldownSeconds() * 1000)); return new GemUseResult(true, successful, target, defaultMessage(successful ? gem.successTip() : gem.failTip(), languages.text(successful ? "gem.tip.success" : "gem.tip.fail", successful ? "&a宝石使用成功" : "&c宝石使用失败"))); } catch (RewardException exception) { return GemUseResult.rejected(target, exception.getMessage()); } } public GemUseResult remove(Player player, String gemName, ItemStack target) { Optional found = registry.find(gemName); if (found.isEmpty()) return GemUseResult.rejected(target, languages.text("cmagic.gem_not_found", "&c未知宝石: {gem}", "gem", gemName)); GemDefinition gem = found.get(); MagicGemUseEvent event = new MagicGemUseEvent(player, gem, false); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) return GemUseResult.rejected(target, languages.text("cmagic.remove_cancelled", "&c宝石拆卸已被其他插件取消")); Map embedded = new LinkedHashMap<>(itemCodec.embedded(target)); int count = embedded.getOrDefault(gem.name(), 0); if (count <= 0) return GemUseResult.rejected(target, languages.text("cmagic.not_embedded", "&c装备未镶嵌该宝石")); boolean broken = roll(gem.breakExpression(), player, target); RewardContext context = new RewardContext(player, gem, target, true); try { ItemStack changed = rewards.applyItemRewards(context, gem.rewards(), Directive.Phase.REMOVE); embedded.compute(gem.name(), (ignored, value) -> value == null || value <= 1 ? null : value - 1); changed = itemCodec.setEmbedded(changed, embedded); context.item(changed); RewardContext onRemove = new RewardContext(player, gem, changed, false); rewards.applyPlayerRewards(onRemove, gem.rewards(), Directive.Phase.REMOVE); if (!broken) give(player, gem, 1); return new GemUseResult(false, !broken, changed, broken ? defaultMessage(gem.breakTip(), languages.text("gem.tip.break", "&c宝石在拆卸时破碎")) : defaultMessage(gem.removeTip(), languages.text("gem.embed.success", "&a宝石拆卸成功"))); } catch (RewardException exception) { return GemUseResult.rejected(target, exception.getMessage()); } } public void give(Player player, GemDefinition gem, int amount) { int remaining = Math.max(1, amount); while (remaining > 0) { int stack = Math.min(remaining, gem.material().getMaxStackSize()); ItemStack item = itemCodec.createGem(gem, stack); player.getInventory().addItem(item).values().forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left)); remaining -= stack; } } private GemDefinition resolveRandom(GemDefinition gem) { return resolveRandom(gem, new HashSet<>()); } private GemDefinition resolveRandom(GemDefinition gem, Set visiting) { if (gem.type() != GemType.RANDOM_GEM) return gem; if (!visiting.add(gem.name().toLowerCase(java.util.Locale.ROOT))) return null; List candidates = gem.randomGems(); double total = candidates.stream().filter(value -> registry.find(value.name()).isPresent()) .mapToDouble(value -> Math.max(0, value.weight())).sum(); if (total <= 0) return null; double roll = ThreadLocalRandom.current().nextDouble(total); for (GemDefinition.WeightedGem candidate : candidates) { GemDefinition selected = registry.find(candidate.name()).orElse(null); if (selected == null || !Double.isFinite(candidate.weight()) || candidate.weight() <= 0) continue; roll -= candidate.weight(); if (roll <= 0) return resolveRandom(selected, visiting); } return null; } private boolean roll(String source, Player player, ItemStack item) { double chance; try { chance = expression.evaluate(source, player, item, Map.of()); } catch (RuntimeException ignored) { chance = 0; } return ThreadLocalRandom.current().nextDouble(100) < chance; } private String defaultMessage(String configured, String fallback) { return configured == null ? fallback : configured; } private String cooldownTip(String configured, double remaining, double total) { if (configured == null || configured.isBlank()) return "&e冷却中,剩余 " + String.format(java.util.Locale.ROOT, "%.2f", remaining) + " 秒"; int open = configured.indexOf('{'); int close = configured.lastIndexOf('}'); if (open < 0 || close < open) return configured; Map arguments = new LinkedHashMap<>(); for (String token : configured.substring(open + 1, close).split(";")) { int equals = token.indexOf('='); if (equals > 0) arguments.put(token.substring(0, equals).trim().toLowerCase(java.util.Locale.ROOT), token.substring(equals + 1)); } int length; try { length = Math.max(0, Integer.parseInt(arguments.getOrDefault("length", "0"))); } catch (NumberFormatException ignored) { length = 0; } int full = total <= 0 ? 0 : (int) Math.round(length * Math.max(0, Math.min(1, 1 - remaining / total))); String suffix; try { suffix = String.format(java.util.Locale.ROOT, arguments.getOrDefault("s", "%.2fs"), remaining); } catch (RuntimeException ignored) { suffix = String.format(java.util.Locale.ROOT, "%.2fs", remaining); } String message = arguments.getOrDefault("p", "") + arguments.getOrDefault("full", "|").repeat(full) + arguments.getOrDefault("empty", "|").repeat(Math.max(0, length - full)) + suffix; return configured.substring(0, open) + "{m=" + message + '}'; } }