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();