Files
goon-game/level-format/src/main/java/com/projectsacrifice/game/LevelContentTool.java
T

163 lines
8.0 KiB
Java

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("&", "&amp;").replace("<", "&lt;")
.replace(">", "&gt;").replace("\"", "&quot;");
}
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);
}
}
}