Rename internal CMagicGem implementation

This commit is contained in:
Coldsmiles
2026-10-05 01:46:32 +08:00
parent 953c15e380
commit ff9b6ce4f1
68 changed files with 298 additions and 306 deletions
@@ -0,0 +1,252 @@
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<UUID, Map<String, Long>> 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<String> 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<String, Integer> 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<String, Long> 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<GemDefinition> 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<String, Integer> 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<String> visiting) {
if (gem.type() != GemType.RANDOM_GEM) return gem;
if (!visiting.add(gem.name().toLowerCase(java.util.Locale.ROOT))) return null;
List<GemDefinition.WeightedGem> 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<String, String> 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 + '}';
}
}