feat: initial CMagicGem reimplementation
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package cn.infstar.cmagicgem.dsl;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class DirectiveParserTest {
|
||||
@Test
|
||||
void keepsNestedRewardTogether() {
|
||||
Directive directive = new DirectiveParser().parse("Conditional{condition=v>1;reward=Enchant{name=sharpness;level=1}} $ignorable");
|
||||
assertEquals("Conditional", directive.type());
|
||||
assertEquals("Enchant{name=sharpness;level=1}", directive.argument("", "reward"));
|
||||
assertTrue(directive.ignorable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesLegacyRewardPhases() {
|
||||
DirectiveParser parser = new DirectiveParser();
|
||||
assertEquals(Directive.Phase.SUCCESS, parser.parse("Money{amount=1} $onSuccess").phase());
|
||||
assertEquals(Directive.Phase.FAIL, parser.parse("Chat{m=失败} $onFail").phase());
|
||||
assertEquals(Directive.Phase.REMOVE, parser.parse("MaxHealth{amount=-1} $onRemove").phase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesBalancedArgumentQuotes() {
|
||||
Directive directive = new DirectiveParser().parse(
|
||||
"Conditional{condition=\"v>=2&&v<=8\";reward='Chat{m=ok}'}");
|
||||
|
||||
assertEquals("v>=2&&v<=8", directive.argument("", "condition"));
|
||||
assertEquals("Chat{m=ok}", directive.argument("", "reward"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retainsLegacyDelimitedMessageSegments() {
|
||||
DirectiveParser parser = new DirectiveParser();
|
||||
Directive directive = parser.parse("Title{messages=主标题;副标题;第二页;第二页副标题;interval=20;stay=60}");
|
||||
|
||||
assertEquals(List.of("主标题", "副标题", "第二页", "第二页副标题"),
|
||||
parser.delimitedArgument(directive, Set.of("messages", "message", "m"),
|
||||
Set.of("interval", "fadein", "fadeout", "stay")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retainsSwitchCycleAfterNamedFirstEntry() {
|
||||
Directive directive = new DirectiveParser().parse("Switch{s=精准;时运}");
|
||||
|
||||
assertEquals("精准", directive.argument("", "s"));
|
||||
assertEquals(List.of("时运"), directive.positional());
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitsLegacyMultiLineLoreGroups() {
|
||||
DirectiveParser parser = new DirectiveParser();
|
||||
Directive directive = parser.parse("LoreMultiReplace{old=第一行;第二行;new=新一行;新二行;limit=v<5}");
|
||||
|
||||
assertEquals(List.of("第一行", "第二行"),
|
||||
parser.delimitedArgument(directive, Set.of("old"), Set.of("lore", "new", "limit")));
|
||||
assertEquals(List.of("新一行", "新二行"),
|
||||
parser.delimitedArgument(directive, Set.of("new"), Set.of("limit")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.infstar.cmagicgem.expression;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ExpressionTest {
|
||||
private final Expression expression = new Expression();
|
||||
|
||||
@Test
|
||||
void evaluatesArithmeticAndVariables() {
|
||||
assertEquals(7, expression.evaluate("v*2+1", Map.of("v", 3.0)));
|
||||
assertEquals(9, expression.evaluate("(v+1)^2", Map.of("v", 2.0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evaluatesConditions() {
|
||||
assertTrue(expression.test("v>=2&&v<=8", Map.of("v", 5.0)));
|
||||
assertFalse(expression.test("v%3==0", Map.of("v", 5.0)));
|
||||
assertFalse(expression.test("false&&v>0", Map.of("v", 5.0)));
|
||||
assertTrue(expression.test("true||v<0", Map.of("v", 5.0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evaluatesLegacyFunctionsAndTernary() {
|
||||
assertEquals(8, expression.evaluate("true?$POW(2,3):0", Map.of()));
|
||||
assertEquals(2, expression.evaluate("false?1:$FLOOR(2.9)", Map.of()));
|
||||
double random = expression.evaluate("$RANDOM()", Map.of());
|
||||
assertTrue(random >= 0 && random < 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.infstar.cmagicgem.gem;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class GemTypeTest {
|
||||
@Test
|
||||
void acceptsLegacyAndEnumSpellings() {
|
||||
assertEquals(GemType.ITEM_GEM, GemType.parse("ItemGem"));
|
||||
assertEquals(GemType.PLAYER_GEM, GemType.parse("PlayerGem"));
|
||||
assertEquals(GemType.RANDOM_GEM, GemType.parse("RandomGem"));
|
||||
assertEquals(GemType.PLAYER_GEM, GemType.parse("PLAYER_GEM"));
|
||||
assertEquals(GemType.RANDOM_GEM, GemType.parse("random-gem"));
|
||||
assertEquals(GemType.ITEM_GEM, GemType.parse("item gem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsBlankValuesToItemGems() {
|
||||
assertEquals(GemType.ITEM_GEM, GemType.parse(null));
|
||||
assertEquals(GemType.ITEM_GEM, GemType.parse(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownValues() {
|
||||
assertThrows(IllegalArgumentException.class, () -> GemType.parse("not-a-gem"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.infstar.cmagicgem.gem;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RandomGemValidatorTest {
|
||||
@Test
|
||||
void acceptsNestedRandomGemsWithOneTerminalType() {
|
||||
Map<String, GemDefinition> gems = gems(
|
||||
gem("item", GemType.ITEM_GEM),
|
||||
random("nested", new GemDefinition.WeightedGem("item", 1)),
|
||||
random("root", new GemDefinition.WeightedGem("nested", 1)));
|
||||
|
||||
assertTrue(RandomGemValidator.validate(gems).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCyclesUnknownReferencesAndMixedTypes() {
|
||||
Map<String, GemDefinition> gems = gems(
|
||||
gem("item", GemType.ITEM_GEM), gem("player", GemType.PLAYER_GEM),
|
||||
random("mixed", new GemDefinition.WeightedGem("item", 1), new GemDefinition.WeightedGem("player", 1)),
|
||||
random("unknown", new GemDefinition.WeightedGem("missing", 1)),
|
||||
random("cycle-a", new GemDefinition.WeightedGem("cycle-b", 1)),
|
||||
random("cycle-b", new GemDefinition.WeightedGem("cycle-a", 1)));
|
||||
|
||||
Map<String, String> errors = RandomGemValidator.validate(gems);
|
||||
assertEquals(Set.of("mixed", "unknown", "cycle-a", "cycle-b"), errors.keySet());
|
||||
}
|
||||
|
||||
private Map<String, GemDefinition> gems(GemDefinition... definitions) {
|
||||
Map<String, GemDefinition> result = new LinkedHashMap<>();
|
||||
for (GemDefinition definition : definitions) result.put(definition.name(), definition);
|
||||
return result;
|
||||
}
|
||||
|
||||
private GemDefinition random(String name, GemDefinition.WeightedGem... targets) {
|
||||
return gem(name, GemType.RANDOM_GEM, List.of(targets));
|
||||
}
|
||||
|
||||
private GemDefinition gem(String name, GemType type) {
|
||||
return gem(name, type, List.of());
|
||||
}
|
||||
|
||||
private GemDefinition gem(String name, GemType type, List<GemDefinition.WeightedGem> targets) {
|
||||
return new GemDefinition(name, name, type, Material.STONE, name, List.of(), null, null, null,
|
||||
null, false, Set.of(), List.of(), 0, "100", "0", "0", null, false,
|
||||
false, 0, null, false, targets, List.of(), List.of(), null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.infstar.cmagicgem.reward;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class RewardContextTest {
|
||||
@Test
|
||||
void evaluatesEachDynamicRewardValueOnce() {
|
||||
RewardContext context = new RewardContext(null, null, null, false);
|
||||
Object reward = new Object();
|
||||
AtomicInteger evaluations = new AtomicInteger();
|
||||
|
||||
assertEquals(7.5, context.evaluatedNumber(reward, () -> {
|
||||
evaluations.incrementAndGet();
|
||||
return 7.5;
|
||||
}));
|
||||
assertEquals(7.5, context.evaluatedNumber(reward, () -> 99));
|
||||
assertEquals(1, evaluations.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package cn.infstar.cmagicgem.skill;
|
||||
|
||||
import cn.infstar.cmagicgem.dsl.Directive;
|
||||
import cn.infstar.cmagicgem.dsl.DirectiveParser;
|
||||
import cn.infstar.cmagicgem.reward.RewardEngine;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BundledConfigurationTest {
|
||||
private final DirectiveParser parser = new DirectiveParser();
|
||||
|
||||
@Test
|
||||
void pluginDescriptorOnlyRegistersNewCommandNames() {
|
||||
YamlConfiguration descriptor = YamlConfiguration.loadConfiguration(new File("src/main/resources/plugin.yml"));
|
||||
|
||||
assertNotNull(descriptor.getConfigurationSection("commands.cmgem"));
|
||||
assertTrue(descriptor.getStringList("commands.cmgem.aliases").contains("cmg"));
|
||||
assertNull(descriptor.getConfigurationSection("commands.mgem"));
|
||||
assertNull(descriptor.getConfigurationSection("commands.magicgem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyBundledGemRewardAndReferenceLoads() throws Exception {
|
||||
Path directory = Path.of("src/main/resources/gems");
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
try (var files = Files.list(directory)) {
|
||||
for (Path file : files.filter(path -> path.toString().endsWith(".yml")).toList()) {
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file.toFile());
|
||||
for (String key : yaml.getKeys(false)) {
|
||||
ConfigurationSection section = yaml.getConfigurationSection(key);
|
||||
if (section == null) continue;
|
||||
names.add(section.getString("Name", key).toLowerCase(Locale.ROOT));
|
||||
for (String source : section.getStringList("Rewards")) {
|
||||
Directive reward = parser.parse(source);
|
||||
assertTrue(RewardEngine.supports(reward.type()), file + "/" + key + ": " + source);
|
||||
}
|
||||
for (String source : section.getStringList("Conditions")) assertFalse(parser.parse(source).type().isBlank());
|
||||
}
|
||||
}
|
||||
}
|
||||
try (var files = Files.list(directory)) {
|
||||
for (Path file : files.filter(path -> path.toString().endsWith(".yml")).toList()) {
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file.toFile());
|
||||
for (String key : yaml.getKeys(false)) {
|
||||
ConfigurationSection random = yaml.getConfigurationSection(key + ".Gems");
|
||||
if (random == null) continue;
|
||||
for (String target : random.getKeys(false))
|
||||
assertTrue(names.contains(target.toLowerCase(Locale.ROOT)), file + "/" + key + " -> " + target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyBundledSkillActionParses() throws Exception {
|
||||
SkillRegistry registry = new SkillRegistry(null, parser);
|
||||
try (var files = Files.list(Path.of("src/main/resources/skills"))) {
|
||||
for (Path path : files.filter(file -> file.toString().endsWith(".yml")).toList()) {
|
||||
File file = path.toFile();
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
|
||||
for (String key : yaml.getKeys(false)) {
|
||||
if (key.equalsIgnoreCase("depend")) continue;
|
||||
ConfigurationSection section = yaml.getConfigurationSection(key);
|
||||
assertNotNull(section, file + "/" + key);
|
||||
for (String source : section.getStringList("Skills")) {
|
||||
SkillAction action = registry.parseAction(source);
|
||||
assertFalse(action.mechanic().type().isBlank(), file + "/" + key + ": " + source);
|
||||
if (action.mechanic().type().equalsIgnoreCase("Reward")
|
||||
|| action.mechanic().type().equalsIgnoreCase("RewardSwitch")) {
|
||||
String nested = action.mechanic().positional().isEmpty()
|
||||
? action.mechanic().argument("", "reward", "r")
|
||||
: action.mechanic().positional().getFirst();
|
||||
assertTrue(RewardEngine.supports(parser.parse(nested).type()), file + "/" + key + ": " + source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.infstar.cmagicgem.skill;
|
||||
|
||||
import cn.infstar.cmagicgem.dsl.DirectiveParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class SkillRegistryTest {
|
||||
private final SkillRegistry registry = new SkillRegistry(null, new DirectiveParser());
|
||||
|
||||
@Test
|
||||
void preservesMythicMechanicAndTargeterWithSpaces() {
|
||||
String line = "message{m=\"Player <mob.name> used a skill\"} @PlayersInRadius{r=20;targetself=true} ~onUse";
|
||||
|
||||
SkillAction action = registry.parseAction(line);
|
||||
|
||||
assertEquals("message", action.mechanic().type());
|
||||
assertEquals("Player <mob.name> used a skill", action.mechanic().argument("", "m").replace("\"", ""));
|
||||
assertEquals("message{m=\"Player <mob.name> used a skill\"} @PlayersInRadius{r=20;targetself=true}",
|
||||
action.mechanicLine());
|
||||
assertEquals(TriggerType.USE, action.trigger());
|
||||
assertEquals("playersinradius", action.target());
|
||||
}
|
||||
|
||||
@Test
|
||||
void separatesTriggerFiltersAndWorldLimits() {
|
||||
SkillAction action = registry.parseAction(
|
||||
"PotionBuff{type=SPEED;level=$LORE:Speed:1$} ~onHit:ARROW;TRIDENT @Self $world:3");
|
||||
|
||||
assertEquals("PotionBuff{type=SPEED;level=$LORE:Speed:1$} @Self", action.mechanicLine());
|
||||
assertEquals(TriggerType.HIT, action.trigger());
|
||||
assertEquals(java.util.List.of("ARROW", "TRIDENT"), action.triggerFilters());
|
||||
assertEquals(java.util.Map.of("world", 3), action.worldLimits());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user