281 lines
8.5 KiB
JavaScript
281 lines
8.5 KiB
JavaScript
const state = {
|
|
definitions: [],
|
|
rules: [],
|
|
activeType: null,
|
|
activeFile: null,
|
|
};
|
|
|
|
const elements = {
|
|
refreshButton: document.getElementById("refresh-button"),
|
|
filterInput: document.getElementById("filter-input"),
|
|
definitionsList: document.getElementById("definitions-list"),
|
|
rulesList: document.getElementById("rules-list"),
|
|
definitionsCount: document.getElementById("definitions-count"),
|
|
rulesCount: document.getElementById("rules-count"),
|
|
editorTitle: document.getElementById("editor-title"),
|
|
editorSubtitle: document.getElementById("editor-subtitle"),
|
|
editorTextarea: document.getElementById("editor-textarea"),
|
|
schemaSummary: document.getElementById("schema-summary"),
|
|
formatButton: document.getElementById("format-button"),
|
|
saveButton: document.getElementById("save-button"),
|
|
statusMessage: document.getElementById("status-message"),
|
|
newRuleInput: document.getElementById("new-rule-id"),
|
|
createRuleButton: document.getElementById("create-rule"),
|
|
buildButton: document.getElementById("build-button"),
|
|
buildOutput: document.getElementById("build-output"),
|
|
};
|
|
|
|
const fetchJson = async (url, options = {}) => {
|
|
const response = await fetch(url, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...options,
|
|
});
|
|
if (!response.ok) {
|
|
let detail = "Request failed.";
|
|
try {
|
|
const body = await response.json();
|
|
detail = body.detail || detail;
|
|
} catch (err) {
|
|
detail = response.statusText || detail;
|
|
}
|
|
throw new Error(detail);
|
|
}
|
|
return response.json();
|
|
};
|
|
|
|
const setStatus = (message, isError = false) => {
|
|
elements.statusMessage.textContent = message;
|
|
elements.statusMessage.classList.toggle("error", isError);
|
|
};
|
|
|
|
const clearSchema = () => {
|
|
elements.schemaSummary.innerHTML = "";
|
|
elements.schemaSummary.classList.add("hidden");
|
|
};
|
|
|
|
const renderSchema = (schema) => {
|
|
if (!schema || schema.type !== "object") {
|
|
clearSchema();
|
|
return;
|
|
}
|
|
const fields = Object.keys(schema.fields || {}).slice(0, 12);
|
|
elements.schemaSummary.innerHTML = "";
|
|
fields.forEach((field) => {
|
|
const chip = document.createElement("span");
|
|
chip.textContent = field;
|
|
elements.schemaSummary.appendChild(chip);
|
|
});
|
|
elements.schemaSummary.classList.toggle("hidden", fields.length === 0);
|
|
};
|
|
|
|
const createNavItem = (file, isActive, onClick) => {
|
|
const button = document.createElement("button");
|
|
button.className = "nav-item";
|
|
if (isActive) {
|
|
button.classList.add("active");
|
|
}
|
|
const label = document.createElement("span");
|
|
label.textContent = file;
|
|
button.appendChild(label);
|
|
button.addEventListener("click", () => onClick(file));
|
|
return button;
|
|
};
|
|
|
|
const renderLists = () => {
|
|
const filterValue = elements.filterInput.value.trim().toLowerCase();
|
|
const defFiles = state.definitions.filter((file) =>
|
|
file.toLowerCase().includes(filterValue),
|
|
);
|
|
const ruleFiles = state.rules.filter((file) =>
|
|
file.toLowerCase().includes(filterValue),
|
|
);
|
|
|
|
elements.definitionsList.innerHTML = "";
|
|
elements.rulesList.innerHTML = "";
|
|
defFiles.forEach((file) => {
|
|
elements.definitionsList.appendChild(
|
|
createNavItem(file, state.activeType === "definition" && state.activeFile === file, openDefinition),
|
|
);
|
|
});
|
|
ruleFiles.forEach((file) => {
|
|
elements.rulesList.appendChild(
|
|
createNavItem(file, state.activeType === "rule" && state.activeFile === file, openRule),
|
|
);
|
|
});
|
|
|
|
elements.definitionsCount.textContent = String(defFiles.length);
|
|
elements.rulesCount.textContent = String(ruleFiles.length);
|
|
};
|
|
|
|
const setEditorState = ({ title, subtitle, content, mode }) => {
|
|
elements.editorTitle.textContent = title;
|
|
elements.editorSubtitle.textContent = subtitle;
|
|
elements.editorTextarea.value = content;
|
|
elements.formatButton.classList.toggle("hidden", mode !== "definition");
|
|
};
|
|
|
|
const openDefinition = async (filename) => {
|
|
try {
|
|
setStatus("Loading definition...");
|
|
const [contentResponse, schemaResponse] = await Promise.all([
|
|
fetchJson(`/api/files/content?filename=${encodeURIComponent(filename)}`),
|
|
fetchJson(`/api/files/schema?filename=${encodeURIComponent(filename)}`),
|
|
]);
|
|
state.activeType = "definition";
|
|
state.activeFile = filename;
|
|
renderLists();
|
|
const rawContent = JSON.stringify(contentResponse.content || {}, null, 2);
|
|
setEditorState({
|
|
title: filename,
|
|
subtitle: filename.endsWith(".tres") ? "Godot resource (tres)" : "JSON definition",
|
|
content: rawContent,
|
|
mode: "definition",
|
|
});
|
|
renderSchema(schemaResponse.schema);
|
|
setStatus("Definition loaded.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to load definition.", true);
|
|
}
|
|
};
|
|
|
|
const openRule = async (filename) => {
|
|
try {
|
|
setStatus("Loading rule...");
|
|
const response = await fetchJson(
|
|
`/api/rules/content?filename=${encodeURIComponent(filename)}`,
|
|
);
|
|
state.activeType = "rule";
|
|
state.activeFile = filename;
|
|
renderLists();
|
|
setEditorState({
|
|
title: filename,
|
|
subtitle: "C# rule source",
|
|
content: response.content || "",
|
|
mode: "rule",
|
|
});
|
|
clearSchema();
|
|
setStatus("Rule loaded.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to load rule.", true);
|
|
}
|
|
};
|
|
|
|
const refreshAll = async () => {
|
|
try {
|
|
setStatus("Refreshing lists...");
|
|
const [definitions, rules] = await Promise.all([
|
|
fetchJson("/api/files/definitions"),
|
|
fetchJson("/api/rules/list"),
|
|
]);
|
|
state.definitions = definitions.files || [];
|
|
state.rules = rules.files || [];
|
|
renderLists();
|
|
setStatus("Lists refreshed.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to refresh.", true);
|
|
}
|
|
};
|
|
|
|
const saveCurrent = async () => {
|
|
if (!state.activeType || !state.activeFile) {
|
|
setStatus("Select a file before saving.", true);
|
|
return;
|
|
}
|
|
if (state.activeType === "definition") {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(elements.editorTextarea.value);
|
|
} catch (err) {
|
|
setStatus("Invalid JSON. Fix errors before saving.", true);
|
|
return;
|
|
}
|
|
try {
|
|
await fetchJson("/api/files/content", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
filename: state.activeFile,
|
|
content: parsed,
|
|
}),
|
|
});
|
|
setStatus("Definition saved.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to save definition.", true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await fetchJson("/api/rules/content", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
filename: state.activeFile,
|
|
content: elements.editorTextarea.value,
|
|
}),
|
|
});
|
|
setStatus("Rule saved.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to save rule.", true);
|
|
}
|
|
};
|
|
|
|
const formatJson = () => {
|
|
if (state.activeType !== "definition") {
|
|
return;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(elements.editorTextarea.value);
|
|
elements.editorTextarea.value = JSON.stringify(parsed, null, 2);
|
|
setStatus("Formatted JSON.");
|
|
} catch (err) {
|
|
setStatus("Invalid JSON. Cannot format.", true);
|
|
}
|
|
};
|
|
|
|
const createRule = async () => {
|
|
const ruleId = elements.newRuleInput.value.trim();
|
|
if (!ruleId) {
|
|
setStatus("Enter a rule id first.", true);
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetchJson("/api/rules/create", {
|
|
method: "POST",
|
|
body: JSON.stringify({ rule_id: ruleId }),
|
|
});
|
|
elements.newRuleInput.value = "";
|
|
await refreshAll();
|
|
if (response.filename) {
|
|
await openRule(response.filename);
|
|
}
|
|
setStatus("Rule created.");
|
|
} catch (err) {
|
|
setStatus(err.message || "Failed to create rule.", true);
|
|
}
|
|
};
|
|
|
|
const buildGame = async () => {
|
|
try {
|
|
elements.buildOutput.textContent = "Running build...";
|
|
const response = await fetchJson("/api/build", { method: "POST" });
|
|
const output = [response.stdout, response.stderr].filter(Boolean).join("\n");
|
|
elements.buildOutput.textContent = output || "Build finished.";
|
|
setStatus(response.success ? "Build complete." : "Build completed with errors.");
|
|
} catch (err) {
|
|
elements.buildOutput.textContent = "Build failed to start.";
|
|
setStatus(err.message || "Build failed.", true);
|
|
}
|
|
};
|
|
|
|
const init = () => {
|
|
elements.refreshButton.addEventListener("click", refreshAll);
|
|
elements.filterInput.addEventListener("input", renderLists);
|
|
elements.saveButton.addEventListener("click", saveCurrent);
|
|
elements.formatButton.addEventListener("click", formatJson);
|
|
elements.createRuleButton.addEventListener("click", createRule);
|
|
elements.buildButton.addEventListener("click", buildGame);
|
|
refreshAll();
|
|
setStatus("Ready.");
|
|
};
|
|
|
|
init();
|