Initial REM planning editor
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
COPY server.mjs ./
|
||||
COPY star-map.html ./
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV DATA_DIR=/data
|
||||
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.mjs"]
|
||||
@@ -0,0 +1,257 @@
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const projectDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const systemNames = {
|
||||
li01: "New York",
|
||||
li02: "California",
|
||||
li03: "Colorado",
|
||||
li04: "Texas",
|
||||
li05: "Alaska",
|
||||
li06: "New Nevada",
|
||||
li07: "Puerto Rico",
|
||||
br01: "New London",
|
||||
br02: "Cambridge",
|
||||
br03: "Manchester",
|
||||
br04: "Leeds",
|
||||
br05: "Edinburgh",
|
||||
br06: "Dublin",
|
||||
ku01: "New Tokyo",
|
||||
ku02: "Shikoku",
|
||||
ku03: "Kyushu",
|
||||
ku04: "Honshu",
|
||||
ku05: "Hokkaido",
|
||||
ku06: "Chugoku",
|
||||
ku07: "Tohoku",
|
||||
rh01: "New Berlin",
|
||||
rh02: "Frankfurt",
|
||||
rh03: "Stuttgart",
|
||||
rh04: "Hamburg",
|
||||
rh05: "Dresden",
|
||||
iw01: "Bering",
|
||||
iw02: "Hudson",
|
||||
iw03: "Magellan",
|
||||
iw04: "Cortez",
|
||||
iw05: "Kepler",
|
||||
iw06: "Galileo",
|
||||
bw01: "Omega-3",
|
||||
bw02: "Omega-5",
|
||||
bw03: "Omega-7",
|
||||
bw04: "Omega-11",
|
||||
bw05: "Omega-41",
|
||||
bw06: "Sigma-13",
|
||||
bw07: "Sigma-17",
|
||||
bw08: "Sigma-19",
|
||||
bw09: "Tau-31",
|
||||
bw10: "Tau-29",
|
||||
bw11: "Sigma 2",
|
||||
ew01: "Tau-37",
|
||||
ew02: "Omicron Alpha",
|
||||
ew03: "Omicron Gamma",
|
||||
ew04: "Omicron Theta",
|
||||
ew05: "Unknown Gamma",
|
||||
ew06: "Unknown Delta",
|
||||
hi01: "Unknown Alpha",
|
||||
hi02: "Omicron Beta",
|
||||
st01: "Omicron Minor",
|
||||
st02: "Omicron Beta",
|
||||
st02c: "Unknown Beta",
|
||||
st03: "Omicron Major",
|
||||
st03b: "Omicron Major",
|
||||
st04: "Omicron Lambda",
|
||||
st05: "Omicron Prime",
|
||||
fp7_system: "Omicron Major"
|
||||
};
|
||||
|
||||
function stripComment(line) {
|
||||
return line.replace(/;.*/, "").trim();
|
||||
}
|
||||
|
||||
function parseIniBlocks(text) {
|
||||
const blocks = [];
|
||||
let current = null;
|
||||
|
||||
for (const rawLine of text.split(/\r?\n/)) {
|
||||
const line = stripComment(rawLine);
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const header = line.match(/^\[([^\]]+)]$/);
|
||||
if (header) {
|
||||
current = { section: header[1].toLowerCase(), values: {} };
|
||||
blocks.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pair = line.match(/^([^=]+?)\s*=\s*(.*)$/);
|
||||
if (!pair) {
|
||||
continue;
|
||||
}
|
||||
const key = pair[1].trim().toLowerCase();
|
||||
const value = pair[2].trim();
|
||||
if (!current.values[key]) {
|
||||
current.values[key] = [];
|
||||
}
|
||||
current.values[key].push(value);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function firstValue(block, key) {
|
||||
return block.values[key]?.[0];
|
||||
}
|
||||
|
||||
function parseGridPosition(value) {
|
||||
const parts = value.split(",").map((part) => Number(part.trim()));
|
||||
if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) {
|
||||
throw new Error(`Invalid system pos: ${value}`);
|
||||
}
|
||||
return { gridX: parts[0], gridY: parts[1] };
|
||||
}
|
||||
|
||||
function makeConnectionId(from, to) {
|
||||
return `c-${from}-${to}`;
|
||||
}
|
||||
|
||||
function normalizePair(from, to) {
|
||||
return [from, to].sort((a, b) => a.localeCompare(b)).join("|");
|
||||
}
|
||||
|
||||
async function readDirectConnections(universeDir, systemsById) {
|
||||
const connections = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const system of systemsById.values()) {
|
||||
const systemFile = resolve(universeDir, system.file.replaceAll("\\", "/"));
|
||||
let text;
|
||||
try {
|
||||
text = await readFile(systemFile, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const block of parseIniBlocks(text)) {
|
||||
if (block.section !== "object") {
|
||||
continue;
|
||||
}
|
||||
const gotoValues = block.values.goto || [];
|
||||
const archetype = (firstValue(block, "archetype") || "").toLowerCase();
|
||||
const type = archetype.includes("hole") ? "neutral" : "standard";
|
||||
|
||||
for (const gotoValue of gotoValues) {
|
||||
const targetId = gotoValue.split(",")[0]?.trim().toLowerCase();
|
||||
if (!targetId || !systemsById.has(targetId) || targetId === system.id) {
|
||||
continue;
|
||||
}
|
||||
const key = normalizePair(system.id, targetId);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
const [from, to] = key.split("|");
|
||||
connections.push({
|
||||
id: makeConnectionId(from, to),
|
||||
from,
|
||||
to,
|
||||
type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connections.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
export async function buildUniverseMap(options = {}) {
|
||||
const universeDir = options.universeDir || "C:/PROJECTS/REM/REM-Mod/DATA/UNIVERSE";
|
||||
const universePath = join(universeDir, "universe.ini");
|
||||
const text = await readFile(universePath, "utf8");
|
||||
const systemBlocks = parseIniBlocks(text).filter((block) => block.section === "system");
|
||||
const parsedSystems = systemBlocks.map((block) => {
|
||||
const nickname = firstValue(block, "nickname");
|
||||
const file = firstValue(block, "file");
|
||||
const pos = firstValue(block, "pos");
|
||||
if (!nickname || !file || !pos) {
|
||||
throw new Error("System block is missing nickname/file/pos");
|
||||
}
|
||||
const id = nickname.toLowerCase();
|
||||
const { gridX, gridY } = parseGridPosition(pos);
|
||||
return {
|
||||
id,
|
||||
nickname,
|
||||
name: systemNames[id] || nickname,
|
||||
file,
|
||||
gridX,
|
||||
gridY,
|
||||
stridName: Number(firstValue(block, "strid_name") || 0),
|
||||
navMapScale: Number(firstValue(block, "navmapscale") || 1),
|
||||
visit: Number(firstValue(block, "visit") || 0),
|
||||
note: ""
|
||||
};
|
||||
});
|
||||
|
||||
const minCol = Math.min(...parsedSystems.map((system) => system.gridX));
|
||||
const minRow = Math.min(...parsedSystems.map((system) => system.gridY));
|
||||
const maxCol = Math.max(...parsedSystems.map((system) => system.gridX));
|
||||
const maxRow = Math.max(...parsedSystems.map((system) => system.gridY));
|
||||
const cellSize = 72;
|
||||
const margin = 72;
|
||||
const grid = { minCol, minRow, maxCol, maxRow, cellSize, margin };
|
||||
|
||||
const systems = parsedSystems.map((system) => ({
|
||||
...system,
|
||||
x: margin + (system.gridX - minCol) * cellSize,
|
||||
y: margin + (system.gridY - minRow) * cellSize
|
||||
})).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
||||
|
||||
const systemsById = new Map(systems.map((system) => [system.id, system]));
|
||||
const connections = await readDirectConnections(universeDir, systemsById);
|
||||
|
||||
return {
|
||||
source: "universe.ini",
|
||||
background: "#05080f",
|
||||
grid,
|
||||
viewBox: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: margin * 2 + (maxCol - minCol) * cellSize,
|
||||
height: margin * 2 + (maxRow - minRow) * cellSize
|
||||
},
|
||||
systems,
|
||||
connections
|
||||
};
|
||||
}
|
||||
|
||||
async function updateProjectFiles() {
|
||||
const mapData = await buildUniverseMap();
|
||||
const htmlPath = join(projectDir, "star-map.html");
|
||||
const dataPath = join(projectDir, "data", "map.json");
|
||||
const html = await readFile(htmlPath, "utf8");
|
||||
const json = JSON.stringify(mapData, null, 2);
|
||||
const nextHtml = html.replace(
|
||||
/<script type="application\/json" id="initial-map-data">[\s\S]*?<\/script>/,
|
||||
`<script type="application/json" id="initial-map-data">\n${json}\n </script>`
|
||||
);
|
||||
|
||||
if (nextHtml === html) {
|
||||
throw new Error("Could not replace initial-map-data in star-map.html");
|
||||
}
|
||||
|
||||
await writeFile(htmlPath, nextHtml, "utf8");
|
||||
await mkdir(dirname(dataPath), { recursive: true });
|
||||
await writeFile(dataPath, `${json}\n`, "utf8");
|
||||
return mapData;
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const mapData = await updateProjectFiles();
|
||||
console.log(`Imported ${mapData.systems.length} systems and ${mapData.connections.length} connections from universe.ini`);
|
||||
}
|
||||
+1434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "editable-star-map",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"import:universe": "node build-universe-map.mjs",
|
||||
"start": "node server.mjs",
|
||||
"test": "node --test star-map.test.mjs server.test.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { createServer as createHttpServer } from "node:http";
|
||||
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const projectDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function sendJson(response, statusCode, value) {
|
||||
response.writeHead(statusCode, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": "no-store"
|
||||
});
|
||||
response.end(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function sendText(response, statusCode, text, contentType = "text/plain; charset=utf-8") {
|
||||
response.writeHead(statusCode, {
|
||||
"content-type": contentType,
|
||||
"cache-control": "no-store"
|
||||
});
|
||||
response.end(text);
|
||||
}
|
||||
|
||||
async function readRequestBody(request, limitBytes = 5 * 1024 * 1024) {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
|
||||
for await (const chunk of request) {
|
||||
size += chunk.length;
|
||||
if (size > limitBytes) {
|
||||
throw new Error("Payload zu gross");
|
||||
}
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
function validateMapData(value) {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new Error("Karte muss ein Objekt sein");
|
||||
}
|
||||
if (!Array.isArray(value.systems) || !Array.isArray(value.connections)) {
|
||||
throw new Error("systems und connections muessen Arrays sein");
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
for (const system of value.systems) {
|
||||
if (!system || typeof system !== "object") {
|
||||
throw new Error("Ungueltiges System");
|
||||
}
|
||||
if (typeof system.id !== "string" || typeof system.name !== "string") {
|
||||
throw new Error("System braucht id und name");
|
||||
}
|
||||
if (!Number.isFinite(system.x) || !Number.isFinite(system.y)) {
|
||||
throw new Error("System braucht x/y Koordinaten");
|
||||
}
|
||||
ids.add(system.id);
|
||||
}
|
||||
|
||||
for (const connection of value.connections) {
|
||||
if (!connection || typeof connection !== "object") {
|
||||
throw new Error("Ungueltige Verbindung");
|
||||
}
|
||||
if (typeof connection.id !== "string" || typeof connection.from !== "string" || typeof connection.to !== "string") {
|
||||
throw new Error("Verbindung braucht id/from/to");
|
||||
}
|
||||
if (!ids.has(connection.from) || !ids.has(connection.to)) {
|
||||
throw new Error("Verbindung verweist auf unbekanntes System");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
source: typeof value.source === "string" ? value.source : undefined,
|
||||
background: typeof value.background === "string" ? value.background : "#05080f",
|
||||
grid: value.grid && typeof value.grid === "object" ? value.grid : undefined,
|
||||
viewBox: value.viewBox && typeof value.viewBox === "object" ? value.viewBox : undefined,
|
||||
systems: value.systems,
|
||||
connections: value.connections
|
||||
};
|
||||
}
|
||||
|
||||
async function readInitialMap(htmlPath) {
|
||||
const html = await readFile(htmlPath, "utf8");
|
||||
const match = html.match(/<script type="application\/json" id="initial-map-data">([\s\S]*?)<\/script>/);
|
||||
if (!match) {
|
||||
throw new Error("initial-map-data nicht gefunden");
|
||||
}
|
||||
return validateMapData(JSON.parse(match[1]));
|
||||
}
|
||||
|
||||
export function createMapServer(options = {}) {
|
||||
const htmlPath = options.htmlPath || join(projectDir, "star-map.html");
|
||||
const dataDir = options.dataDir || process.env.DATA_DIR || join(projectDir, "data");
|
||||
const dataPath = join(dataDir, "map.json");
|
||||
|
||||
return createHttpServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
|
||||
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/star-map.html")) {
|
||||
const html = await readFile(htmlPath, "utf8");
|
||||
sendText(response, 200, html, "text/html; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/map" && request.method === "GET") {
|
||||
if (existsSync(dataPath)) {
|
||||
sendJson(response, 200, validateMapData(JSON.parse(await readFile(dataPath, "utf8"))));
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, await readInitialMap(htmlPath));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/map" && request.method === "PUT") {
|
||||
const body = await readRequestBody(request);
|
||||
const mapData = validateMapData(JSON.parse(body));
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
const tempPath = `${dataPath}.tmp`;
|
||||
await writeFile(tempPath, `${JSON.stringify(mapData, null, 2)}\n`, "utf8");
|
||||
await rename(tempPath, dataPath);
|
||||
sendJson(response, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(response, 404, { error: "Nicht gefunden" });
|
||||
} catch (error) {
|
||||
const status = error instanceof SyntaxError || /ungueltig|braucht|muss|verweist|Payload|systems/i.test(error.message)
|
||||
? 400
|
||||
: 500;
|
||||
sendJson(response, status, { error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const host = process.env.HOST || "127.0.0.1";
|
||||
const server = createMapServer();
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Star Map Editor: http://${host}:${port}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { createMapServer } from "./server.mjs";
|
||||
|
||||
test("server stores and returns the editable map JSON", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "star-map-"));
|
||||
const server = createMapServer({ dataDir });
|
||||
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const { port } = server.address();
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
background: "#101010",
|
||||
systems: [{ id: "alpha", name: "Alpha", x: 10, y: 20, note: "saved" }],
|
||||
connections: []
|
||||
};
|
||||
|
||||
const saveResponse = await fetch(`${baseUrl}/api/map`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
assert.equal(saveResponse.status, 200);
|
||||
assert.deepEqual(await saveResponse.json(), { ok: true });
|
||||
|
||||
const loadResponse = await fetch(`${baseUrl}/api/map`);
|
||||
assert.equal(loadResponse.status, 200);
|
||||
assert.deepEqual(await loadResponse.json(), payload);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("server rejects invalid map payloads", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "star-map-"));
|
||||
const server = createMapServer({ dataDir });
|
||||
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const { port } = server.address();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/map`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ systems: "bad", connections: [] })
|
||||
});
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+2620
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { buildUniverseMap } from "./build-universe-map.mjs";
|
||||
|
||||
const html = readFileSync(new URL("./star-map.html", import.meta.url), "utf8");
|
||||
const startScript = readFileSync(new URL("./start-webserver.cmd", import.meta.url), "utf8");
|
||||
|
||||
function extractJsonScript(id) {
|
||||
const pattern = new RegExp(`<script type="application/json" id="${id}">([\\s\\S]*?)</script>`);
|
||||
const match = html.match(pattern);
|
||||
assert.ok(match, `missing JSON script #${id}`);
|
||||
return JSON.parse(match[1]);
|
||||
}
|
||||
|
||||
test("universe import builds a grid map from universe.ini", async () => {
|
||||
const data = await buildUniverseMap({
|
||||
universeDir: "C:/PROJECTS/REM/REM-Mod/DATA/UNIVERSE"
|
||||
});
|
||||
|
||||
assert.equal(data.source, "universe.ini");
|
||||
assert.equal(data.systems.length, 58);
|
||||
assert.ok(data.connections.length >= 80, "expected direct jump connections from system INIs");
|
||||
assert.deepEqual(data.grid, {
|
||||
minCol: 0,
|
||||
minRow: 0,
|
||||
maxCol: 16,
|
||||
maxRow: 16,
|
||||
cellSize: 72,
|
||||
margin: 72
|
||||
});
|
||||
|
||||
const newYork = data.systems.find((system) => system.id === "li01");
|
||||
assert.ok(newYork, "missing Li01");
|
||||
assert.equal(newYork.name, "New York");
|
||||
assert.equal(newYork.gridX, 7);
|
||||
assert.equal(newYork.gridY, 8);
|
||||
assert.equal(newYork.x, 576);
|
||||
assert.equal(newYork.y, 648);
|
||||
|
||||
assert.ok(data.connections.some((connection) => connection.from === "li01" && connection.to === "li02"));
|
||||
});
|
||||
|
||||
test("template contains the systems and connections from universe.ini", () => {
|
||||
const data = extractJsonScript("initial-map-data");
|
||||
|
||||
assert.equal(data.source, "universe.ini");
|
||||
assert.equal(data.systems.length, 58, "expected active systems from universe.ini");
|
||||
assert.ok(data.connections.length >= 80, "expected direct route set from system INIs");
|
||||
assert.ok(data.grid, "expected grid metadata");
|
||||
|
||||
for (const name of ["Tau-37", "Leeds", "New Tokyo", "Sigma-19", "New Berlin", "Omicron Prime"]) {
|
||||
assert.ok(data.systems.some((system) => system.name === name), `missing ${name}`);
|
||||
}
|
||||
|
||||
for (const connection of data.connections) {
|
||||
assert.ok(data.systems.some((system) => system.id === connection.from), `missing source ${connection.from}`);
|
||||
assert.ok(data.systems.some((system) => system.id === connection.to), `missing target ${connection.to}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("editor UI exposes creation, deletion, linking, notes, and background controls", () => {
|
||||
assert.match(html, /data-mode="add"/, "missing add-system mode");
|
||||
assert.match(html, /data-mode="connect"/, "missing connection mode");
|
||||
assert.match(html, /id="deleteSelected"/, "missing delete action");
|
||||
assert.match(html, /id="systemNote"/, "missing per-system note field");
|
||||
assert.match(html, /id="backgroundColor"/, "missing configurable background");
|
||||
assert.match(html, /id="saveRemote"/, "missing persistent save action");
|
||||
assert.match(html, /id="loadRemote"/, "missing persistent load action");
|
||||
assert.doesNotMatch(html, /id="snapToGrid"/, "grid snapping must not be optional");
|
||||
assert.match(html, /function snapPointToGrid/, "missing grid snapping behavior");
|
||||
assert.doesNotMatch(html, /snapToGrid\.checked/, "grid snapping must always be active");
|
||||
assert.match(html, /function createSystem/, "missing system creation behavior");
|
||||
assert.match(html, /function createConnection/, "missing connection creation behavior");
|
||||
assert.match(html, /async function saveRemoteMap/, "missing server save behavior");
|
||||
assert.match(html, /async function loadRemoteMap/, "missing server load behavior");
|
||||
});
|
||||
|
||||
test("system dragging has a forgiving hit target and stable pointer handlers", () => {
|
||||
assert.match(html, /class", "system-hit"/, "missing large system drag hit target");
|
||||
assert.match(html, /function handleDragMove/, "missing shared drag move handler");
|
||||
assert.match(html, /window\.addEventListener\("pointermove", handleDragMove\)/, "missing window pointermove handler");
|
||||
assert.match(html, /window\.addEventListener\("pointerup", handleDragEnd\)/, "missing window pointerup handler");
|
||||
});
|
||||
|
||||
test("start script launches the local server and opens the browser", () => {
|
||||
assert.match(startScript, /node.*server\.mjs/i, "start script must launch server.mjs with node");
|
||||
assert.match(startScript, /127\.0\.0\.1:3000/i, "start script must open the local app URL");
|
||||
assert.match(startScript, /Start-Process/i, "start script must open the browser");
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "APP_DIR=%~dp0"
|
||||
set "PORT=3000"
|
||||
set "APP_URL=http://127.0.0.1:3000/"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$ErrorActionPreference = 'Stop';" ^
|
||||
"$appDir = $env:APP_DIR;" ^
|
||||
"$port = [int]$env:PORT;" ^
|
||||
"$url = $env:APP_URL;" ^
|
||||
"$listener = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue;" ^
|
||||
"if (-not $listener) {" ^
|
||||
" Start-Process -FilePath 'node' -ArgumentList 'server.mjs' -WorkingDirectory $appDir -WindowStyle Hidden;" ^
|
||||
" Start-Sleep -Milliseconds 900;" ^
|
||||
"}" ^
|
||||
"Start-Process $url;"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Fehler: Server konnte nicht gestartet oder Browser nicht geoeffnet werden.
|
||||
echo Pruefe, ob Node.js installiert ist und Port %PORT% frei ist.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
endlocal
|
||||
Reference in New Issue
Block a user