feat: add multiplayer capture vertical slice
This commit is contained in:
@@ -16,6 +16,7 @@ java {
|
||||
dependencies {
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.20.0")
|
||||
|
||||
testImplementation(project(":engine"))
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
@@ -20,6 +20,12 @@ import java.util.regex.Pattern;
|
||||
public final class ContentValidator {
|
||||
private static final Pattern STABLE_ID = Pattern.compile("^[a-z][a-z0-9_-]*:[a-z0-9][a-z0-9_-]*$");
|
||||
private static final Pattern HEX_COLOR = Pattern.compile("^#[0-9A-Fa-f]{6}$");
|
||||
static final Set<String> ENGINE_ELEMENT_TYPES = Set.of(
|
||||
"NORMAL", "GRASS", "POISON", "FIRE", "FLYING", "WATER", "ELECTRIC", "PSYCHIC",
|
||||
"GROUND", "ROCK", "ICE", "BUG", "FIGHTING", "GHOST", "DRAGON"
|
||||
);
|
||||
static final Set<String> ENGINE_RARITIES = Set.of("COMMON", "UNCOMMON", "RARE", "EPIC", "LEGENDARY");
|
||||
static final Set<String> ENGINE_MOVE_CATEGORIES = Set.of("PHYSICAL", "SPECIAL", "STATUS");
|
||||
|
||||
static final Set<String> UNOFFICIAL_SLICE_SPECIES = Set.of(
|
||||
"projectcapture:bulbasaur",
|
||||
@@ -165,11 +171,24 @@ public final class ContentValidator {
|
||||
if (!id.isEmpty() && roles.containsKey(id)) {
|
||||
report.add("DUPLICATE_ID", location + ".id", "Duplicate move ID: " + id);
|
||||
}
|
||||
requireText(move, "name", location + ".name", report);
|
||||
requireEnum(move, "element", Set.of("NORMAL", "GRASS", "TOXIN", "FIRE", "AIR", "WATER", "ELECTRIC"), location, report);
|
||||
requireCurrentSchema(move, location, report);
|
||||
rejectLegacyField(move, "name", location, report);
|
||||
rejectLegacyField(move, "element", location, report);
|
||||
requireText(move, "displayName", location + ".displayName", report);
|
||||
requireEnum(move, "type", ENGINE_ELEMENT_TYPES, location, report);
|
||||
String category = requireEnum(move, "category", ENGINE_MOVE_CATEGORIES, location, report);
|
||||
int power = requireIntegerRange(move, "power", 0, 250, location, report);
|
||||
requireNumberRange(move, "accuracy", 0.0, 1.0, location, report);
|
||||
requireIntegerRange(move, "priority", -7, 7, location, report);
|
||||
String role = requireEnum(move, "role", Set.of("BASIC", "COMMAND"), location, report);
|
||||
requireNumberRange(move, "power", 0, 250, location, report);
|
||||
requireNumberRange(move, "cooldownSeconds", 0.1, 120, location, report);
|
||||
if (!category.isEmpty() && !"STATUS".equals(category) && power == 0) {
|
||||
report.add(
|
||||
"MOVE_POWER_CATEGORY",
|
||||
location + ".power",
|
||||
"Engine damaging moves must have positive power"
|
||||
);
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
roles.putIfAbsent(id, role);
|
||||
}
|
||||
@@ -298,14 +317,18 @@ public final class ContentValidator {
|
||||
if (!id.isEmpty() && !ids.add(id)) {
|
||||
report.add("DUPLICATE_ID", location + ".id", "Duplicate species ID: " + id);
|
||||
}
|
||||
requireCurrentSchema(species, location, report);
|
||||
requireText(species, "displayName", location + ".displayName", report);
|
||||
int catalogNumber = requireIntegerRange(species, "catalogNumber", 1, 9999, location, report);
|
||||
if (catalogNumber >= 0 && !catalogNumbers.add(catalogNumber)) {
|
||||
report.add("DUPLICATE_CATALOG_NUMBER", location + ".catalogNumber", "Duplicate catalog number: " + catalogNumber);
|
||||
}
|
||||
requireEnum(species, "form", Set.of("BASE", "STAGE_1", "STAGE_2", "SINGLE"), location, report);
|
||||
requireNonEmptyTextArray(species.path("elements"), location + ".elements", report);
|
||||
requireNumberRange(species, "captureDifficulty", 0.01, 0.99, location, report);
|
||||
rejectLegacyField(species, "elements", location, report);
|
||||
rejectLegacyField(species, "captureDifficulty", location, report);
|
||||
requireEnumArray(species.path("types"), ENGINE_ELEMENT_TYPES, 1, 2, location + ".types", report);
|
||||
requireEnum(species, "rarity", ENGINE_RARITIES, location, report);
|
||||
requireIntegerRange(species, "captureRate", 1, 255, location, report);
|
||||
JsonNode stats = species.path("baseStats");
|
||||
for (String stat : List.of("vitality", "power", "guard", "agility")) {
|
||||
requireIntegerRange(stats, stat, 1, 255, location + ".baseStats", report);
|
||||
@@ -594,6 +617,58 @@ public final class ContentValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCurrentSchema(JsonNode node, String location, ValidationReport report) {
|
||||
JsonNode version = node.path("schemaVersion");
|
||||
if (!version.isIntegralNumber() || version.asInt() != ContentBundle.CURRENT_SCHEMA_VERSION) {
|
||||
report.add(
|
||||
"SCHEMA_VERSION",
|
||||
location + ".schemaVersion",
|
||||
"Expected engine-compatible schemaVersion " + ContentBundle.CURRENT_SCHEMA_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectLegacyField(JsonNode node, String field, String location, ValidationReport report) {
|
||||
if (node.has(field)) {
|
||||
report.add(
|
||||
"LEGACY_FIELD",
|
||||
location + "." + field,
|
||||
"Legacy field is not allowed in the engine-aligned schema"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireEnumArray(
|
||||
JsonNode node,
|
||||
Set<String> allowed,
|
||||
int minimumSize,
|
||||
int maximumSize,
|
||||
String location,
|
||||
ValidationReport report
|
||||
) {
|
||||
requireNonEmptyTextArray(node, location, report);
|
||||
if (!node.isArray()) {
|
||||
return;
|
||||
}
|
||||
if (node.size() < minimumSize || node.size() > maximumSize) {
|
||||
report.add(
|
||||
"ARRAY_SIZE",
|
||||
location,
|
||||
"Expected " + minimumSize + ".." + maximumSize + " values"
|
||||
);
|
||||
}
|
||||
for (int index = 0; index < node.size(); index++) {
|
||||
JsonNode value = node.path(index);
|
||||
if (value.isTextual() && !allowed.contains(value.asText())) {
|
||||
report.add(
|
||||
"ENUM_VALUE",
|
||||
location + "[" + index + "]",
|
||||
"Expected one of " + allowed + ", got " + value.asText()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireNonEmptyTextArray(JsonNode node, String location, ValidationReport report) {
|
||||
if (!node.isArray() || node.isEmpty()) {
|
||||
report.add("TEXT_ARRAY", location, "Expected a non-empty text array");
|
||||
|
||||
@@ -3,6 +3,9 @@ package dev.projectcapture.contenttools;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import dev.projectcapture.engine.model.ElementType;
|
||||
import dev.projectcapture.engine.model.MoveDefinition;
|
||||
import dev.projectcapture.engine.model.SpeciesDefinition;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -46,6 +49,45 @@ class ContentValidatorTest {
|
||||
assertEquals(ContentValidator.UNOFFICIAL_SLICE_SPECIES, ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allTwelveSpeciesUseEngineElementRarityAndCaptureSchema() throws IOException {
|
||||
JsonNode species = ContentBundle.load(contentRoot).document("species.json").path("items");
|
||||
|
||||
assertEquals(12, species.size());
|
||||
for (JsonNode definition : species) {
|
||||
assertEquals(SpeciesDefinition.CURRENT_SCHEMA_VERSION, definition.path("schemaVersion").asInt());
|
||||
assertTrue(definition.path("types").size() >= 1 && definition.path("types").size() <= 2);
|
||||
for (JsonNode type : definition.path("types")) {
|
||||
ElementType.valueOf(type.asText());
|
||||
}
|
||||
SpeciesDefinition.Rarity.valueOf(definition.path("rarity").asText());
|
||||
assertTrue(definition.path("captureRate").asInt() >= 1);
|
||||
assertTrue(definition.path("captureRate").asInt() <= 255);
|
||||
assertFalse(definition.has("elements"));
|
||||
assertFalse(definition.has("captureDifficulty"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void allMovesConstructAsEngineMoveDefinitions() throws IOException {
|
||||
JsonNode moves = ContentBundle.load(contentRoot).document("moves.json").path("items");
|
||||
|
||||
for (JsonNode move : moves) {
|
||||
new MoveDefinition(
|
||||
move.path("schemaVersion").asInt(),
|
||||
move.path("id").asText(),
|
||||
move.path("displayName").asText(),
|
||||
ElementType.valueOf(move.path("type").asText()),
|
||||
MoveDefinition.Category.valueOf(move.path("category").asText()),
|
||||
move.path("power").asInt(),
|
||||
move.path("accuracy").asDouble(),
|
||||
move.path("priority").asInt()
|
||||
);
|
||||
assertFalse(move.has("name"));
|
||||
assertFalse(move.has("element"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongSchemaAndDuplicateIds() throws IOException {
|
||||
ObjectNode moves = object("moves.json");
|
||||
@@ -60,6 +102,40 @@ class ContentValidatorTest {
|
||||
assertTrue(report.containsCode("DUPLICATE_ID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonEngineSpeciesEnumsAndCaptureRate() throws IOException {
|
||||
ObjectNode species = object("species.json");
|
||||
ObjectNode first = (ObjectNode) species.path("items").path(0);
|
||||
((ArrayNode) first.path("types")).set(0, ContentBundle.json().getNodeFactory().textNode("TOXIN"));
|
||||
first.put("rarity", "MYTHIC");
|
||||
first.put("captureRate", 0);
|
||||
write("species.json", species);
|
||||
|
||||
ValidationReport report = new ContentValidator().validate(contentRoot);
|
||||
|
||||
assertTrue(report.containsCode("ENUM_VALUE"));
|
||||
assertTrue(report.containsCode("INTEGER_RANGE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonEngineMoveCombatFields() throws IOException {
|
||||
ObjectNode moves = object("moves.json");
|
||||
ObjectNode first = (ObjectNode) moves.path("items").path(0);
|
||||
first.put("type", "AIR");
|
||||
first.put("category", "SPECIAL");
|
||||
first.put("power", 0);
|
||||
first.put("accuracy", 1.1);
|
||||
first.put("priority", 8);
|
||||
write("moves.json", moves);
|
||||
|
||||
ValidationReport report = new ContentValidator().validate(contentRoot);
|
||||
|
||||
assertTrue(report.containsCode("ENUM_VALUE"));
|
||||
assertTrue(report.containsCode("MOVE_POWER_CATEGORY"));
|
||||
assertTrue(report.containsCode("NUMBER_RANGE"));
|
||||
assertTrue(report.containsCode("INTEGER_RANGE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownMoveEvolutionAndAssetReferences() throws IOException {
|
||||
ObjectNode species = object("species.json");
|
||||
|
||||
Reference in New Issue
Block a user