Rename internal CMagicGem implementation
This commit is contained in:
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user