Завершить контентную архитектуру этапа 4
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/** Desktop entrypoint for deterministic validation and content export. */
|
||||
public final class LevelContentTool {
|
||||
private LevelContentTool() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = run(args, System.out, System.err);
|
||||
if (exitCode != 0) throw new IllegalStateException("Level content tool failed with exit " + exitCode);
|
||||
}
|
||||
|
||||
public static int run(String[] args, PrintStream output, PrintStream errors) {
|
||||
if (output == null || errors == null) throw new IllegalArgumentException("output streams are required");
|
||||
try {
|
||||
if (args != null && args.length == 2 && "validate".equals(args[0])) {
|
||||
List<LevelDefinition> levels = loadDirectory(args[1]);
|
||||
output.println("Validated " + levels.size() + " level(s); format="
|
||||
+ LevelFormat.CURRENT_FORMAT_VERSION + "; physics="
|
||||
+ LevelFormat.CURRENT_PHYSICS_VERSION);
|
||||
return 0;
|
||||
}
|
||||
if (args != null && args.length == 3 && "preview".equals(args[0])) {
|
||||
List<LevelDefinition> levels = loadDirectory(args[1]);
|
||||
Path destination = Paths.get(args[2]).toAbsolutePath().normalize();
|
||||
writePreview(levels, destination);
|
||||
output.println("Exported " + levels.size() + " level preview(s) to " + destination);
|
||||
return 0;
|
||||
}
|
||||
errors.println("Usage: validate <levels-directory> | preview <levels-directory> <output-directory>");
|
||||
return 2;
|
||||
} catch (IllegalArgumentException | IOException error) {
|
||||
errors.println(error.getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LevelDefinition> loadDirectory(String value) {
|
||||
Path directory = Paths.get(value).toAbsolutePath().normalize();
|
||||
return LevelCatalogLoader.load(new DirectorySource(directory), "index.json");
|
||||
}
|
||||
|
||||
private static void writePreview(List<LevelDefinition> levels, Path destination) throws IOException {
|
||||
Files.createDirectories(destination);
|
||||
StringBuilder coordinates = new StringBuilder(
|
||||
"level\tkind\tindex\ttype\tx\ty\twidth\theight\ttravelX\ttravelY\tcontrolOrSignals\n");
|
||||
for (LevelDefinition level : levels) {
|
||||
Files.writeString(destination.resolve(String.format("level-%02d.svg", level.number)),
|
||||
svg(level), StandardCharsets.UTF_8);
|
||||
coordinates.append(coordinates(level));
|
||||
}
|
||||
Files.writeString(destination.resolve("catalog.tsv"), coordinates.toString(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String svg(LevelDefinition level) {
|
||||
StringBuilder value = new StringBuilder();
|
||||
value.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
||||
.append("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ")
|
||||
.append(number(level.worldWidth)).append(' ').append(number(level.worldHeight))
|
||||
.append("\" data-format-version=\"").append(level.formatVersion)
|
||||
.append("\" data-physics-version=\"").append(level.physicsVersion).append("\">\n")
|
||||
.append(" <title>").append(xml(level.number + " — " + level.title)).append("</title>\n")
|
||||
.append(" <rect width=\"100%\" height=\"100%\" fill=\"#111827\"/>\n");
|
||||
for (int i = 0; i < level.platforms.size(); i++) {
|
||||
rect(value, "platform", i, level.platforms.get(i).box, "#64748b");
|
||||
}
|
||||
for (int i = 0; i < level.hazards.size(); i++) {
|
||||
rect(value, "hazard-" + level.hazards.get(i).type, i,
|
||||
level.hazards.get(i).box, "#ef4444");
|
||||
}
|
||||
for (int i = 0; i < level.switches.size(); i++) {
|
||||
rect(value, "switch-" + level.switches.get(i).type, i,
|
||||
level.switches.get(i).box, "#eab308");
|
||||
}
|
||||
for (int i = 0; i < level.doors.size(); i++) {
|
||||
rect(value, "door", i, level.doors.get(i).box, "#a855f7");
|
||||
}
|
||||
rect(value, "spawn", 0, new LevelDefinition.Box(level.spawnX, level.spawnY, 62f, 100f), "#22c55e");
|
||||
rect(value, "goal", 0, level.goal, "#38bdf8");
|
||||
value.append("</svg>\n");
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private static void rect(StringBuilder output, String kind, int index,
|
||||
LevelDefinition.Box box, String color) {
|
||||
output.append(" <rect data-kind=\"").append(kind).append("\" data-index=\"")
|
||||
.append(index).append("\" x=\"").append(number(box.x)).append("\" y=\"")
|
||||
.append(number(box.y)).append("\" width=\"").append(number(box.width))
|
||||
.append("\" height=\"").append(number(box.height)).append("\" fill=\"")
|
||||
.append(color).append("\" fill-opacity=\"0.78\"/>\n");
|
||||
}
|
||||
|
||||
private static String coordinates(LevelDefinition level) {
|
||||
StringBuilder value = new StringBuilder();
|
||||
row(value, level.number, "spawn", 0, "PLAYER", new LevelDefinition.Box(
|
||||
level.spawnX, level.spawnY, 62f, 100f), 0f, 0f, "");
|
||||
row(value, level.number, "goal", 0, "GOAL", level.goal, 0f, 0f, "");
|
||||
for (int i = 0; i < level.platforms.size(); i++) {
|
||||
LevelDefinition.Platform item = level.platforms.get(i);
|
||||
row(value, level.number, "platform", i, item.isMoving() ? "MOVING" : "STATIC",
|
||||
item.box, item.travelX, item.travelY, item.controlId);
|
||||
}
|
||||
for (int i = 0; i < level.hazards.size(); i++) {
|
||||
LevelDefinition.Hazard item = level.hazards.get(i);
|
||||
row(value, level.number, "hazard", i, item.type.name(), item.box,
|
||||
item.impulseX, item.impulseY, item.relayId);
|
||||
}
|
||||
for (int i = 0; i < level.switches.size(); i++) {
|
||||
LevelDefinition.Switch item = level.switches.get(i);
|
||||
row(value, level.number, "switch", i, item.type.name(), item.box,
|
||||
0f, 0f, item.signalId);
|
||||
}
|
||||
for (int i = 0; i < level.doors.size(); i++) {
|
||||
LevelDefinition.Door item = level.doors.get(i);
|
||||
row(value, level.number, "door", i, "DOOR", item.box,
|
||||
0f, 0f, String.join("+", item.requiredSignals));
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private static void row(StringBuilder output, int level, String kind, int index, String type,
|
||||
LevelDefinition.Box box, float travelX, float travelY, String control) {
|
||||
output.append(level).append('\t').append(kind).append('\t').append(index).append('\t')
|
||||
.append(type).append('\t').append(number(box.x)).append('\t')
|
||||
.append(number(box.y)).append('\t').append(number(box.width)).append('\t')
|
||||
.append(number(box.height)).append('\t').append(number(travelX)).append('\t')
|
||||
.append(number(travelY)).append('\t').append(control == null ? "" : control).append('\n');
|
||||
}
|
||||
|
||||
private static String number(float value) {
|
||||
if (value == (long) value) return Long.toString((long) value);
|
||||
return Float.toString(value);
|
||||
}
|
||||
|
||||
private static String xml(String value) {
|
||||
return value.replace("&", "&").replace("<", "<")
|
||||
.replace(">", ">").replace("\"", """);
|
||||
}
|
||||
|
||||
private static final class DirectorySource implements LevelResourceSource {
|
||||
private final Path directory;
|
||||
|
||||
DirectorySource(Path directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader open(String path) throws IOException {
|
||||
Path resolved = directory.resolve(path).normalize();
|
||||
if (!resolved.startsWith(directory) || !Files.isRegularFile(resolved)) return null;
|
||||
return Files.newBufferedReader(resolved, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user