Initial commit

moved from private version control
This commit is contained in:
2025-10-19 21:12:41 +11:00
parent 5e84773bbe
commit e64f3e881e
187 changed files with 25372 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
/**
* @typedef {Object} PinDescriptor
* @property {string} id Unique pin identifier scoped to the node.
* @property {string} name Public display name.
* @property {('input'|'output')} direction Pin direction relative to the node.
* @property {('exec'|'number'|'boolean'|'string'|'table'|'any')} kind Pin kind describing blueprint behavior.
* @property {string} [description] Optional tooltip/description.
* @property {unknown} [defaultValue] Default value for data pins.
*/
/**
* @typedef {Object} BlueprintNodeInit
* @property {string} id Node identifier.
* @property {string} type Node definition identifier.
* @property {string} title Display title for the node.
* @property {{x:number,y:number}} position Node position within the workspace.
* @property {Array<PinDescriptor>} inputs Input pin descriptors.
* @property {Array<PinDescriptor>} outputs Output pin descriptors.
* @property {Record<string, unknown>} properties Arbitrary node properties.
*/
/**
* Represents a blueprint node instance within the graph.
*/
export class BlueprintNode {
/**
* @param {BlueprintNodeInit} init Initialization payload.
*/
constructor(init) {
this.id = init.id;
this.type = init.type;
this.title = init.title;
this.position = { ...init.position };
this.inputs = init.inputs.map((pin) => ({ ...pin }));
this.outputs = init.outputs.map((pin) => ({ ...pin }));
this.properties = { ...init.properties };
}
/**
* Updates the node position immutably.
*
* @param {{x:number,y:number}} position New position coordinates.
*/
setPosition(position) {
this.position = { x: position.x, y: position.y };
}
/**
* Provides a shallow clone suitable for serialization.
*
* @returns {BlueprintNodeInit}
*/
toJSON() {
return {
id: this.id,
type: this.type,
title: this.title,
position: { ...this.position },
inputs: this.inputs.map((pin) => ({ ...pin })),
outputs: this.outputs.map((pin) => ({ ...pin })),
properties: { ...this.properties },
};
}
/**
* Retrieves a pin descriptor matching the supplied identifier.
*
* @param {string} pinId Pin identifier.
* @returns {PinDescriptor | undefined}
*/
getPin(pinId) {
return [...this.inputs, ...this.outputs].find((pin) => pin.id === pinId);
}
}

View File

@@ -0,0 +1,46 @@
/**
* Generates a lightweight unique identifier.
*
* @returns {string}
*/
const createConnectionId = () =>
globalThis.crypto?.randomUUID?.() ??
`conn_${Math.random().toString(36).slice(2, 10)}`;
/**
* @typedef {Object} PinReference
* @property {string} nodeId Linked node identifier.
* @property {string} pinId Pin identifier within the node.
*/
/**
* Represents a directional connection between two node pins.
*/
export class Connection {
/**
* @param {PinReference} from Origin pin (must be output).
* @param {PinReference} to Target pin (must be input).
* @param {('exec'|'number'|'boolean'|'string'|'table'|'any')} kind Pin kind carried by the connection.
* @param {string} [id] Optional connection identifier.
*/
constructor(from, to, kind, id) {
this.id = id ?? createConnectionId();
this.from = { ...from };
this.to = { ...to };
this.kind = kind;
}
/**
* Serializes the connection for persistence.
*
* @returns {{from: PinReference, to: PinReference, kind: Connection['kind']}}
*/
toJSON() {
return {
id: this.id,
from: { ...this.from },
to: { ...this.to },
kind: this.kind,
};
}
}

1181
scripts/core/LuaGenerator.js Normal file

File diff suppressed because it is too large Load Diff

355
scripts/core/NodeGraph.js Normal file
View File

@@ -0,0 +1,355 @@
import { BlueprintNode } from "./BlueprintNode.js";
import { Connection } from "./Connection.js";
/** @typedef {import('./BlueprintNode.js').BlueprintNodeInit} BlueprintNodeInit */
/** @typedef {import('./BlueprintNode.js').PinDescriptor} PinDescriptor */
/** @typedef {import('./Connection.js').PinReference} PinReference */
/**
* Maintains the canonical blueprint data model: nodes, pins, and their connections.
*/
export class NodeGraph extends EventTarget {
constructor() {
super();
/** @type {Map<string, BlueprintNode>} */
this.nodes = new Map();
/** @type {Map<string, Connection>} */
this.connections = new Map();
this.nodeCounters = new Map();
}
/**
* Emits a custom event notifying listeners about a change.
*
* @param {string} type Event name.
* @param {unknown} detail Event payload.
*/
emit(type, detail) {
this.dispatchEvent(new CustomEvent(type, { detail }));
}
/**
* Generates a unique node identifier scoped by node type.
*
* @param {string} type Node definition identifier.
* @returns {string}
*/
createNodeId(type) {
const current = this.nodeCounters.get(type) ?? 0;
const next = current + 1;
this.nodeCounters.set(type, next);
return `${type}_${String(next).padStart(2, "0")}`;
}
/**
* Registers a node instance within the graph.
*
* @param {BlueprintNode} node Node to add.
*/
addNode(node) {
this.nodes.set(node.id, node);
this.emit("nodeschanged", { type: "add", node });
}
/**
* Removes a node and any associated connections.
*
* @param {string} nodeId Target node identifier.
*/
removeNode(nodeId) {
const node = this.nodes.get(nodeId);
if (!node) {
return;
}
[...this.connections.values()].forEach((connection) => {
if (
connection.from.nodeId === nodeId ||
connection.to.nodeId === nodeId
) {
this.connections.delete(connection.id);
}
});
this.nodes.delete(nodeId);
this.emit("nodeschanged", { type: "remove", nodeId });
this.emit("connectionschanged", { type: "prune", nodeId });
}
/**
* Updates the node position within the graph.
*
* @param {string} nodeId Node identifier.
* @param {{x:number,y:number}} position New coordinates to store.
*/
setNodePosition(nodeId, position) {
const node = this.nodes.get(nodeId);
if (!node) {
return;
}
node.setPosition(position);
this.emit("nodepositionchanged", { nodeId, position: { ...position } });
}
/**
* Updates a node property bag value.
*
* @param {string} nodeId Node identifier.
* @param {string} key Property key.
* @param {unknown} value Property value.
*/
setNodeProperty(nodeId, key, value) {
const node = this.nodes.get(nodeId);
if (!node) {
return;
}
node.properties[key] = value;
this.emit("nodepropertychanged", { nodeId, key, value });
}
/**
* Removes an existing connection.
*
* @param {string} connectionId Identifier for the connection.
*/
removeConnection(connectionId) {
if (!this.connections.has(connectionId)) {
return;
}
this.connections.delete(connectionId);
this.emit("connectionschanged", { type: "remove", connectionId });
}
/**
* Removes connections linked to a specific pin.
*
* @param {PinReference} ref Pin reference to clear.
*/
removeConnectionsForPin(ref) {
let mutated = false;
const affectedNodes = new Set();
[...this.connections.values()].forEach((connection) => {
if (
(connection.from.nodeId === ref.nodeId &&
connection.from.pinId === ref.pinId) ||
(connection.to.nodeId === ref.nodeId &&
connection.to.pinId === ref.pinId)
) {
this.connections.delete(connection.id);
mutated = true;
affectedNodes.add(connection.from.nodeId);
affectedNodes.add(connection.to.nodeId);
}
});
if (mutated) {
this.emit("connectionschanged", {
type: "prune",
ref,
nodes: [...affectedNodes],
});
}
}
/**
* Attempts to connect two pins if the contract is valid.
*
* @param {PinReference} from Output pin reference.
* @param {PinReference} to Input pin reference.
* @returns {boolean} Whether the connection was created.
*/
connect(from, to) {
const fromNode = this.nodes.get(from.nodeId);
const toNode = this.nodes.get(to.nodeId);
if (!fromNode || !toNode) {
return false;
}
const fromPin = fromNode.getPin(from.pinId);
const toPin = toNode.getPin(to.pinId);
if (!fromPin || !toPin) {
return false;
}
if (!this.#isConnectionValid(fromNode, fromPin, toNode, toPin)) {
return false;
}
if (fromPin.kind === "exec") {
this.removeConnectionsForPin(from);
}
// Deduplicate single-input connections except for exec inputs which may stack.
if (toPin.kind !== "exec") {
this.removeConnectionsForPin(to);
}
const connection = new Connection(
from,
to,
toPin.kind === "any" ? fromPin.kind : toPin.kind
);
this.connections.set(connection.id, connection);
this.emit("connectionschanged", { type: "add", connection });
return true;
}
/**
* Retrieves an immutable list of nodes.
*
* @returns {Array<BlueprintNode>}
*/
getNodes() {
return [...this.nodes.values()].map((node) => node);
}
/**
* Retrieves all active connections.
*
* @returns {Array<Connection>}
*/
getConnections() {
return [...this.connections.values()].map((connection) => connection);
}
/**
* Returns connections targeting a particular node and optional pin.
*
* @param {string} nodeId Node identifier.
* @param {string} [pinId] Optional pin identifier.
* @returns {Array<Connection>}
*/
getConnectionsForNode(nodeId, pinId) {
return this.getConnections().filter((connection) => {
const matchesNode =
connection.from.nodeId === nodeId || connection.to.nodeId === nodeId;
const matchesPin = !pinId
? true
: connection.from.pinId === pinId || connection.to.pinId === pinId;
return matchesNode && matchesPin;
});
}
/**
* Produces a serializable payload for the graph.
*
* @returns {{nodes: Array<ReturnType<BlueprintNode['toJSON']>>, connections: Array<ReturnType<Connection['toJSON']>>}}
*/
toJSON() {
return {
nodes: this.getNodes().map((node) => node.toJSON()),
connections: this.getConnections().map((connection) =>
connection.toJSON()
),
};
}
/**
* Restores a graph from serialized payload.
*
* @param {{nodes:Array<BlueprintNodeInit>, connections:Array<ReturnType<Connection['toJSON']>>}} payload Serialized graph data.
* @returns {NodeGraph}
*/
static fromJSON(payload) {
const graph = new NodeGraph();
payload.nodes.forEach((nodeData) => {
const node = new BlueprintNode(nodeData);
graph.nodes.set(node.id, node);
});
payload.connections.forEach((connectionData) => {
const connection = new Connection(
connectionData.from,
connectionData.to,
connectionData.kind,
connectionData.id
);
graph.connections.set(connection.id, connection);
});
return graph;
}
/**
* Replaces the current graph contents using the provided serialized payload.
*
* @param {{nodes:Array<BlueprintNodeInit>, connections:Array<ReturnType<Connection['toJSON']>>}} payload Serialized graph data.
*/
replaceState(payload) {
this.nodes.clear();
this.connections.clear();
this.nodeCounters.clear();
payload.nodes.forEach((nodeData) => {
const node = new BlueprintNode(nodeData);
this.nodes.set(node.id, node);
this.#trackNodeCounter(node);
});
payload.connections.forEach((connectionData) => {
const connection = new Connection(
connectionData.from,
connectionData.to,
connectionData.kind,
connectionData.id
);
this.connections.set(connection.id, connection);
});
this.emit("graphrestored", {
nodes: this.getNodes(),
connections: this.getConnections(),
});
}
/**
* Determines whether a connection between the provided pins is permitted.
*
* @param {BlueprintNode} fromNode Source node.
* @param {PinDescriptor} fromPin Source pin descriptor.
* @param {BlueprintNode} toNode Target node.
* @param {PinDescriptor} toPin Target pin descriptor.
* @returns {boolean}
*/
#isConnectionValid(fromNode, fromPin, toNode, toPin) {
if (fromNode.id === toNode.id) {
return false;
}
if (fromPin.direction !== "output" || toPin.direction !== "input") {
return false;
}
if (
toPin.kind !== "any" &&
fromPin.kind !== "any" &&
fromPin.kind !== toPin.kind
) {
return false;
}
// Avoid duplicate identical connections.
const duplicated = [...this.connections.values()].some(
(existing) =>
existing.from.nodeId === fromNode.id &&
existing.from.pinId === fromPin.id &&
existing.to.nodeId === toNode.id &&
existing.to.pinId === toPin.id
);
return !duplicated;
}
/**
* Updates the node counter registry for the supplied node instance.
*
* @param {BlueprintNode} node Node reference used to update counters.
*/
#trackNodeCounter(node) {
const match = /_(\d+)$/.exec(node.id);
const numeric = match ? Number.parseInt(match[1], 10) : 0;
const current = this.nodeCounters.get(node.type) ?? 0;
this.nodeCounters.set(node.type, Math.max(current, numeric));
}
}

469
scripts/main.js Normal file
View File

@@ -0,0 +1,469 @@
import { BlueprintWorkspace } from "./ui/BlueprintWorkspace.js";
import { NodeRegistry } from "./nodes/NodeRegistry.js";
import { LuaGenerator } from "./core/LuaGenerator.js";
/**
* @typedef {{ compileLua(source: string): Promise<{ filePath: string }> }} ElectronAPI
*/
/**
* @template T
* @param {T | null} element DOM element reference.
* @param {string} id Identifier used for error reporting.
* @returns {T}
*/
const requireElement = (element, id) => {
if (!element) {
throw new Error(`Missing required element: ${id}`);
}
return element;
};
const appBodyElement = /** @type {HTMLElement} */ (
requireElement(document.getElementById("appBody"), "appBody")
);
const workspaceElement = /** @type {HTMLElement} */ (
requireElement(document.getElementById("workspaceCanvas"), "workspaceCanvas")
);
const nodeLayer = /** @type {HTMLElement} */ (
requireElement(document.getElementById("nodeLayer"), "nodeLayer")
);
const connectionLayer = /** @type {SVGElement} */ (
requireElement(document.getElementById("connectionLayer"), "connectionLayer")
);
const eventList = /** @type {HTMLUListElement} */ (
requireElement(document.getElementById("eventList"), "eventList")
);
const variableList = /** @type {HTMLUListElement} */ (
requireElement(document.getElementById("variableList"), "variableList")
);
const addVariableButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("addVariableButton"),
"addVariableButton"
)
);
const paletteList = /** @type {HTMLElement} */ (
requireElement(document.getElementById("paletteList"), "paletteList")
);
const paletteSearch = /** @type {HTMLInputElement} */ (
requireElement(document.getElementById("paletteSearch"), "paletteSearch")
);
const paletteElement = /** @type {HTMLElement} */ (
requireElement(document.getElementById("palettePanel"), "palettePanel")
);
const inspectorContent = /** @type {HTMLElement} */ (
requireElement(
document.getElementById("inspectorContent"),
"inspectorContent"
)
);
const duplicateNodeButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("duplicateNodeButton"),
"duplicateNodeButton"
)
);
const deleteNodeButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("deleteNodeButton"),
"deleteNodeButton"
)
);
const compileButton = /** @type {HTMLButtonElement} */ (
requireElement(document.getElementById("compileButton"), "compileButton")
);
const exportLuaButton = /** @type {HTMLButtonElement} */ (
requireElement(document.getElementById("exportLuaButton"), "exportLuaButton")
);
const projectSettingsButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("projectSettingsButton"),
"projectSettingsButton"
)
);
const paletteToggleButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("paletteToggleButton"),
"paletteToggleButton"
)
);
const frameSelectionButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("frameSelectionButton"),
"frameSelectionButton"
)
);
const luaModal = /** @type {HTMLElement} */ (
requireElement(document.getElementById("luaModal"), "luaModal")
);
const luaModalCode = /** @type {HTMLElement} */ (
requireElement(document.getElementById("luaModalCode"), "luaModalCode")
);
const luaModalCodeContainer = /** @type {HTMLElement} */ (
requireElement(
luaModal.querySelector(".lua-modal__code"),
"luaModalCodeContainer"
)
);
const copyLuaButton = /** @type {HTMLButtonElement} */ (
requireElement(document.getElementById("copyLuaButton"), "copyLuaButton")
);
const closeLuaModalButton = /** @type {HTMLButtonElement} */ (
requireElement(
document.getElementById("closeLuaModalButton"),
"closeLuaModalButton"
)
);
const registry = new NodeRegistry();
const generator = new LuaGenerator(registry);
const workspace = new BlueprintWorkspace({
workspaceElement,
nodeLayer,
connectionLayer,
paletteList,
paletteSearch,
paletteElement,
inspectorContent,
duplicateNodeButton,
deleteNodeButton,
generator,
registry,
eventList,
variableList,
addVariableButton,
projectSettingsButton,
paletteToggleButton,
appBodyElement,
frameSelectionButton,
});
workspace.initialize();
/** @type {ElectronAPI | undefined} */
const electronAPI =
typeof window !== "undefined" && "electronAPI" in window
? /** @type {ElectronAPI} */ (window.electronAPI)
: undefined;
document.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
const escapeHtml = (value) =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const luaTokenPattern =
/(--.*$)|("(?:\\.|[^"\\])*")|('(?!\[)(?:\\.|[^'\\])*')|(\b\d+(?:\.\d+)?\b)|\b(and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/gm;
const highlightLua = (source) => {
if (!source) {
return "";
}
let result = "";
let lastIndex = 0;
source.replace(
luaTokenPattern,
(
match,
comment,
doubleQuoted,
singleQuoted,
numberLiteral,
keyword,
offset
) => {
result += escapeHtml(source.slice(lastIndex, offset));
let tokenType = "";
if (comment) {
tokenType = "comment";
} else if (doubleQuoted || singleQuoted) {
tokenType = "string";
} else if (numberLiteral) {
tokenType = "number";
} else {
tokenType = "keyword";
}
result += `<span class="lua-token lua-token--${tokenType}">${escapeHtml(
match
)}</span>`;
lastIndex = offset + match.length;
return match;
}
);
result += escapeHtml(source.slice(lastIndex));
return result;
};
const focusableSelector =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
let isLuaModalOpen = false;
let lastFocusedElement = /** @type {HTMLElement | null} */ (null);
let copyFeedbackTimeout = /** @type {number | null} */ (null);
let currentLuaSource = "";
let compileFeedbackTimeout = /** @type {number | null} */ (null);
const scheduleMicrotask = (callback) => {
if (typeof queueMicrotask === "function") {
queueMicrotask(callback);
return;
}
Promise.resolve().then(callback);
};
const getModalFocusableElements = () => {
return /** @type {Array<HTMLElement>} */ (
Array.from(luaModal.querySelectorAll(focusableSelector)).filter(
(element) =>
element instanceof HTMLElement && !element.hasAttribute("disabled")
)
);
};
const handleModalKeydown = (event) => {
if (!isLuaModalOpen) {
return;
}
if (event.key === "Escape") {
event.preventDefault();
closeLuaModal();
return;
}
if (event.key !== "Tab") {
return;
}
const focusable = getModalFocusableElements();
if (!focusable.length) {
event.preventDefault();
return;
}
const currentIndex = focusable.indexOf(document.activeElement);
let nextIndex = currentIndex;
if (event.shiftKey) {
nextIndex = currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;
} else {
nextIndex =
currentIndex === -1 || currentIndex === focusable.length - 1
? 0
: currentIndex + 1;
}
event.preventDefault();
focusable[nextIndex].focus();
};
const writeClipboard = async (text) => {
if (
navigator.clipboard &&
typeof navigator.clipboard.writeText === "function"
) {
await navigator.clipboard.writeText(text);
return;
}
const selection = document.getSelection();
const previousRange =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
const fallback = document.createElement("textarea");
fallback.value = text;
fallback.setAttribute("readonly", "true");
fallback.style.position = "fixed";
fallback.style.opacity = "0";
document.body.appendChild(fallback);
fallback.select();
document.execCommand("copy");
document.body.removeChild(fallback);
if (previousRange && selection) {
selection.removeAllRanges();
selection.addRange(previousRange);
}
};
const resetCopyFeedback = () => {
copyLuaButton.classList.remove("is-success");
copyLuaButton.setAttribute("aria-label", "Copy to clipboard");
copyLuaButton.setAttribute("title", "Copy to clipboard");
};
const openLuaModal = () => {
currentLuaSource = workspace.exportLua();
luaModalCode.innerHTML = highlightLua(currentLuaSource);
luaModalCodeContainer.scrollTop = 0;
if (copyFeedbackTimeout !== null) {
window.clearTimeout(copyFeedbackTimeout);
copyFeedbackTimeout = null;
}
resetCopyFeedback();
if (isLuaModalOpen) {
return;
}
lastFocusedElement =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
luaModal.removeAttribute("hidden");
luaModal.setAttribute("aria-hidden", "false");
document.body.classList.add("modal-open");
isLuaModalOpen = true;
document.addEventListener("keydown", handleModalKeydown);
scheduleMicrotask(() => {
const focusTarget = luaModalCodeContainer;
focusTarget.focus();
});
};
const closeLuaModal = () => {
if (!isLuaModalOpen) {
return;
}
isLuaModalOpen = false;
luaModal.setAttribute("aria-hidden", "true");
luaModal.setAttribute("hidden", "");
document.body.classList.remove("modal-open");
document.removeEventListener("keydown", handleModalKeydown);
if (copyFeedbackTimeout !== null) {
window.clearTimeout(copyFeedbackTimeout);
copyFeedbackTimeout = null;
}
resetCopyFeedback();
if (lastFocusedElement && document.contains(lastFocusedElement)) {
lastFocusedElement.focus();
}
};
/**
* Restores the compile button to its idle appearance.
*
* @returns {void}
*/
const resetCompileButton = () => {
if (compileFeedbackTimeout !== null) {
window.clearTimeout(compileFeedbackTimeout);
compileFeedbackTimeout = null;
}
compileButton.classList.remove("is-success", "is-error", "is-busy");
compileButton.disabled = false;
compileButton.textContent = "Compile";
};
/**
* Handles compile button clicks by requesting a Lua export from Electron.
*
* @returns {Promise<void>}
*/
const handleCompileClick = async () => {
if (!electronAPI) {
return;
}
if (compileFeedbackTimeout !== null) {
window.clearTimeout(compileFeedbackTimeout);
compileFeedbackTimeout = null;
}
compileButton.classList.remove("is-success", "is-error");
compileButton.classList.add("is-busy");
compileButton.disabled = true;
compileButton.textContent = "Compiling...";
const source = workspace.exportLua();
try {
const result = await electronAPI.compileLua(source);
console.info("Lua compiled to", result.filePath);
compileButton.classList.remove("is-busy");
compileButton.classList.add("is-success");
compileButton.disabled = false;
compileButton.textContent = "Compiled!";
compileFeedbackTimeout = window.setTimeout(() => {
resetCompileButton();
}, 1800);
} catch (error) {
console.error("Failed to compile Lua", error);
compileButton.classList.remove("is-busy");
compileButton.classList.add("is-error");
compileButton.disabled = false;
compileButton.textContent = "Failed";
compileFeedbackTimeout = window.setTimeout(() => {
resetCompileButton();
}, 2200);
}
};
if (electronAPI && typeof electronAPI.compileLua === "function") {
compileButton.disabled = false;
compileButton.addEventListener("click", () => {
handleCompileClick().catch((error) => {
console.error("Unhandled compile error", error);
resetCompileButton();
});
});
} else {
compileButton.disabled = true;
compileButton.title = "Compile is available in the desktop app";
}
exportLuaButton.addEventListener("click", () => {
openLuaModal();
});
luaModal.addEventListener("click", (event) => {
const target =
event.target instanceof HTMLElement
? event.target.closest("[data-modal-close]")
: null;
if (target) {
event.preventDefault();
closeLuaModal();
}
});
copyLuaButton.addEventListener("click", async () => {
if (!currentLuaSource) {
return;
}
try {
await writeClipboard(currentLuaSource);
copyLuaButton.classList.add("is-success");
copyLuaButton.setAttribute("aria-label", "Copied!");
copyLuaButton.setAttribute("title", "Copied!");
if (copyFeedbackTimeout !== null) {
window.clearTimeout(copyFeedbackTimeout);
}
copyFeedbackTimeout = window.setTimeout(() => {
resetCopyFeedback();
copyFeedbackTimeout = null;
}, 1500);
} catch (error) {
console.error("Copy failed", error);
}
});
closeLuaModalButton.addEventListener("click", () => {
closeLuaModal();
});

View File

@@ -0,0 +1,296 @@
import { BlueprintNode } from "../core/BlueprintNode.js";
import { nodeModules } from "./library/index.js";
/** @typedef {import('./nodeTypes.js').NodeDefinition} NodeDefinition */
/** @typedef {import('./nodeTypes.js').NodeBehavior} NodeBehavior */
/** @typedef {import('./nodeTypes.js').NodeModule} NodeModule */
/** @typedef {{ value: string, weight: number }} SearchField */
/**
* Central registry providing node definitions, behavior hooks, and factory helpers.
*/
export class NodeRegistry {
constructor() {
/** @type {Map<string, NodeDefinition>} */
this.definitions = new Map();
/** @type {Map<string, NodeBehavior>} */
this.behaviors = new Map();
/** @type {Set<string>} */
this.entryNodeTypes = new Set();
/** @type {Map<string, '_init'|'_update'|'_draw'|'_update60'>} */
this.entryPointEvents = new Map();
nodeModules.forEach((module) => this.registerModule(module));
}
/**
* Normalizes search inputs for case-insensitive comparisons.
*
* @param {string} query Raw query string.
* @returns {string}
*/
#normalizeQuery(query) {
return (query ?? "").trim().toLowerCase();
}
/**
* Determines whether a node definition matches the provided query.
*
* @param {NodeDefinition} definition Definition to inspect.
* @param {string} query Search query.
* @returns {boolean}
*/
matchesDefinition(definition, query) {
const normalized = this.#normalizeQuery(query);
if (!normalized) {
return true;
}
const score = this.#scoreDefinition(definition, normalized);
return Number.isFinite(score);
}
/**
* Enumerates every registered definition.
*
* @returns {Array<NodeDefinition>}
*/
list() {
return [...this.definitions.values()];
}
/**
* Retrieves a single node definition.
*
* @param {string} id Definition identifier.
* @returns {NodeDefinition | undefined}
*/
get(id) {
return this.definitions.get(id);
}
/**
* Performs a fuzzy search across node metadata and returns results sorted by distance.
*
* @param {string} query Search input.
* @returns {Array<NodeDefinition>}
*/
search(query) {
const normalized = this.#normalizeQuery(query);
if (!normalized) {
return this.list().sort((a, b) => a.title.localeCompare(b.title));
}
/** @type {Array<{ definition: NodeDefinition, score: number }>} */
const ranked = [];
this.definitions.forEach((definition) => {
const score = this.#scoreDefinition(definition, normalized);
if (Number.isFinite(score)) {
ranked.push({ definition, score });
}
});
ranked.sort((a, b) => {
if (a.score === b.score) {
return a.definition.title.localeCompare(b.definition.title);
}
return a.score - b.score;
});
return ranked.map((entry) => entry.definition);
}
/**
* Aggregates searchable fields for a node definition and assigns weighting.
*
* @param {NodeDefinition} definition Definition to inspect.
* @returns {Array<SearchField>} Normalized field collection with weights.
*/
#collectSearchFields(definition) {
const fields = /** @type {Array<SearchField>} */ ([
{ value: definition.title, weight: 0.8 },
{ value: definition.category, weight: 1 },
]);
if (definition.description) {
fields.push({ value: definition.description, weight: 1 });
}
if (Array.isArray(definition.searchTags)) {
definition.searchTags.forEach((tag) => {
fields.push({ value: tag, weight: 1 });
});
}
return fields
.filter((field) => typeof field.value === "string" && field.value.trim().length)
.map((field) => ({
value: field.value.toLowerCase(),
weight: field.weight,
}));
}
/**
* Computes a fuzzy score representing how closely a definition matches a query.
*
* @param {NodeDefinition} definition Target definition.
* @param {string} normalizedQuery Lowercase trimmed query string.
* @returns {number} Matching score where lower values are closer; Infinity means no match.
*/
#scoreDefinition(definition, normalizedQuery) {
const fields = this.#collectSearchFields(definition);
if (!fields.length) {
return Number.POSITIVE_INFINITY;
}
let best = Number.POSITIVE_INFINITY;
for (const field of fields) {
const score = this.#computeFuzzyScore(normalizedQuery, field.value);
if (!Number.isFinite(score)) {
continue;
}
const weighted = score * field.weight;
if (weighted < best) {
best = weighted;
}
if (best === 0) {
break;
}
}
return best;
}
/**
* Produces a fuzzy matching distance between the query and a candidate string.
*
* @param {string} query Normalized query string.
* @param {string} candidate Normalized candidate string.
* @returns {number} Distance score; Infinity indicates the query is not a subsequence of the candidate.
*/
#computeFuzzyScore(query, candidate) {
if (!candidate) {
return Number.POSITIVE_INFINITY;
}
const qLength = query.length;
const cLength = candidate.length;
if (!qLength) {
return 0;
}
let qIndex = 0;
let score = 0;
let lastMatch = -1;
for (let cIndex = 0; cIndex < cLength; cIndex += 1) {
if (candidate[cIndex] !== query[qIndex]) {
continue;
}
if (lastMatch === -1) {
score += cIndex;
} else {
score += cIndex - lastMatch - 1;
}
lastMatch = cIndex;
qIndex += 1;
if (qIndex === qLength) {
break;
}
}
if (qIndex !== qLength) {
return Number.POSITIVE_INFINITY;
}
score += cLength - lastMatch - 1;
return score;
}
/**
* Instantiates a blueprint node from a definition.
*
* @param {string} definitionId Definition identifier.
* @param {{id: string, position: {x:number,y:number}}} options Initialization options.
* @returns {BlueprintNode}
*/
createNode(definitionId, options) {
const definition = this.get(definitionId);
if (!definition) {
throw new Error(`Unknown node definition: ${definitionId}`);
}
const properties = {};
definition.properties.forEach((schema) => {
const value = schema.defaultValue ?? null;
properties[schema.key] = value;
});
definition.initializeProperties?.(properties);
return new BlueprintNode({
id: options.id,
type: definition.id,
title: definition.title,
position: { ...options.position },
inputs: definition.inputs.map((pin) => ({
...pin,
})),
outputs: definition.outputs.map((pin) => ({
...pin,
})),
properties,
});
}
/**
* Registers a node module exposing editor metadata and Lua behavior.
*
* @param {NodeModule} module Node module to register.
*/
registerModule(module) {
const { definition, behavior } = module;
this.definitions.set(definition.id, definition);
if (behavior) {
this.behaviors.set(definition.id, behavior);
if (behavior.isEntryPoint) {
this.entryNodeTypes.add(definition.id);
if (behavior.eventName) {
this.entryPointEvents.set(definition.id, behavior.eventName);
}
}
}
}
/**
* Retrieves behavior metadata for a node type.
*
* @param {string} id Node identifier.
* @returns {NodeBehavior | undefined}
*/
getBehavior(id) {
return this.behaviors.get(id);
}
/**
* Enumerates node types flagged as entry points.
*
* @returns {Array<string>}
*/
getEntryNodeTypes() {
return [...this.entryNodeTypes];
}
/**
* Exposes lifecycle event bindings for entry nodes.
*
* @returns {Map<string, '_init'|'_update'|'_draw'|'_update60'>}
*/
getEntryPointEvents() {
return new Map(this.entryPointEvents);
}
}

View File

@@ -0,0 +1,41 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Adds two numeric values.
*/
export const addNumberNode = createNodeModule(
{
id: "add_number",
title: "Add",
category: "Math",
description: "Add two numbers.",
searchTags: ["add", "math", "sum", "number"],
inputs: [
{
id: "a",
name: "A",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "b",
name: "B",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "res", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `(${a}) + (${b})`;
},
}
);

View File

@@ -0,0 +1,16 @@
import { sfxNode } from "./sfx.js";
import { musicNode } from "./music.js";
/**
* All audio-related node modules backed by PICO-8 helpers.
*/
export const audioNodes = [sfxNode, musicNode];
/**
* Checklist tracking coverage of documented audio helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const audioFunctionChecklist = [
{ function: "SFX", nodeId: "audio_sfx", implemented: true },
{ function: "MUSIC", nodeId: "audio_music", implemented: true },
];

View File

@@ -0,0 +1,64 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Plays or stops music using the PICO-8 music helper.
*/
export const musicNode = createNodeModule(
{
id: "audio_music",
title: "Play Music",
category: "Audio",
description:
"Start, fade, or stop music playback using MUSIC().",
searchTags: ["music", "audio", "song", "pattern"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "pattern",
name: "Pattern",
direction: "input",
kind: "number",
description: "Pattern index or command",
defaultValue: 0,
},
{
id: "fade",
name: "Fade (ms)",
direction: "input",
kind: "number",
description: "Fade duration in milliseconds",
},
{
id: "mask",
name: "Channel Mask",
direction: "input",
kind: "number",
description: "Bitmask reserving channels",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const pattern = resolveValueInput("pattern", "0");
const fade = resolveValueInput("fade", OMIT);
const mask = resolveValueInput("mask", OMIT);
const args = [pattern];
if (fade !== OMIT || mask !== OMIT) {
const fadeArg = fade === OMIT ? "nil" : fade;
args.push(fadeArg);
if (mask !== OMIT) {
args.push(mask);
}
}
const line = `${indent(indentLevel)}music(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,76 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Plays or controls a sound effect using the PICO-8 sfx helper.
*/
export const sfxNode = createNodeModule(
{
id: "audio_sfx",
title: "Play SFX",
category: "Audio",
description:
"Trigger a sound effect, control looping, or stop playback via SFX().",
searchTags: ["sfx", "audio", "sound", "effect"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "id",
name: "SFX",
direction: "input",
kind: "number",
description: "Sound effect slot or command",
defaultValue: 0,
},
{
id: "channel",
name: "Channel",
direction: "input",
kind: "number",
description: "Playback channel (-1 auto)",
},
{
id: "offset",
name: "Offset",
direction: "input",
kind: "number",
description: "Note offset",
},
{
id: "length",
name: "Length",
direction: "input",
kind: "number",
description: "Number of notes to play",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const id = resolveValueInput("id", "0");
const channel = resolveValueInput("channel", OMIT);
const offset = resolveValueInput("offset", OMIT);
const length = resolveValueInput("length", OMIT);
const args = [id];
if (channel !== OMIT || offset !== OMIT || length !== OMIT) {
const channelArg = channel === OMIT ? "nil" : channel;
args.push(channelArg);
if (offset !== OMIT || length !== OMIT) {
const offsetArg = offset === OMIT ? "nil" : offset;
args.push(offsetArg);
if (length !== OMIT) {
args.push(length);
}
}
}
const line = `${indent(indentLevel)}sfx(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,26 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Constant boolean literal.
*/
export const booleanLiteralNode = createNodeModule(
{
id: "boolean_literal",
title: "Boolean",
category: "Values",
description: "Constant boolean literal.",
searchTags: ["boolean", "literal", "true", "false"],
inputs: [],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "boolean" },
],
properties: [
{ key: "value", label: "Value", type: "boolean", defaultValue: true },
],
},
{
evaluateValue: ({ node, formatLiteral }) => {
return formatLiteral("boolean", node.properties.value ?? true);
},
}
);

View File

@@ -0,0 +1,32 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Invokes a custom event by reference.
* @type {import('../nodeTypes.js').NodeModule}
*/
export const callCustomEventNode = createNodeModule(
{
id: "call_custom_event",
title: "Call Custom Event",
category: "Events",
description: "Invokes a custom event defined elsewhere in the graph.",
searchTags: ["event", "call", "trigger", "custom"],
inputs: [{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" }],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
initializeProperties: (properties) => {
properties.eventId =
typeof properties.eventId === "string" ? properties.eventId : "";
if (
!properties.arguments ||
typeof properties.arguments !== "object" ||
Array.isArray(properties.arguments)
) {
properties.arguments = {};
}
},
},
null
);

View File

@@ -0,0 +1,47 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Compares two values using a selected operator.
*/
export const compareNode = createNodeModule(
{
id: "compare",
title: "Compare",
category: "Logic",
description: "Compare two values with a selected operator.",
searchTags: ["compare", "logic", "condition", "branch"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "any" },
{ id: "b", name: "B", direction: "input", kind: "any" },
],
outputs: [
{ id: "res", name: "Result", direction: "output", kind: "boolean" },
],
properties: [
{
key: "operator",
label: "Operator",
type: "enum",
defaultValue: "==",
options: [
{ label: "Equal", value: "==" },
{ label: "Not Equal", value: "!=" },
{ label: "Greater", value: ">" },
{ label: "Less", value: "<" },
{ label: "Greater Or Equal", value: ">=" },
{ label: "Less Or Equal", value: "<=" },
],
},
],
},
{
evaluateValue: ({ node, resolveValueInput, sanitizeOperator }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
const operator = sanitizeOperator(
String(node.properties.operator ?? "==")
);
return `(${a}) ${operator} (${b})`;
},
}
);

View File

@@ -0,0 +1,44 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Defines a reusable custom event entry point.
* @type {import('../nodeTypes.js').NodeModule}
*/
export const customEventNode = createNodeModule(
{
id: "custom_event",
title: "Custom Event",
category: "Events",
description: "Defines a custom event that can be triggered elsewhere.",
searchTags: ["event", "custom", "broadcast", "trigger"],
inputs: [],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [
{
key: "name",
label: "Event Name",
type: "string",
defaultValue: "CustomEvent",
placeholder: "Enter event name",
},
],
initializeProperties: (properties) => {
if (typeof properties.name !== "string" || !properties.name.trim()) {
properties.name = "CustomEvent";
}
if (!Array.isArray(properties.parameters)) {
properties.parameters = [];
}
const counter = Number.isFinite(properties.parameterCounter)
? Number(properties.parameterCounter)
: 0;
properties.parameterCounter = counter;
},
},
{
isEntryPoint: true,
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
}
);

View File

@@ -0,0 +1,33 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Opens persistent cartridge storage using cartdata.
*/
export const cartdataNode = createNodeModule(
{
id: "data_cartdata",
title: "Cart Data Init",
category: "Data",
description: "Setup persistent storage for the cartridge using CARTDATA().",
searchTags: ["cartdata", "save", "persistent", "data"],
inputs: [
{
id: "id",
name: "Identifier",
direction: "input",
kind: "string",
defaultValue: "\"my_cart\"",
},
],
outputs: [
{ id: "loaded", name: "Loaded", direction: "output", kind: "boolean" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const id = resolveValueInput("id", '"pico_cart"');
return `cartdata(${id})`;
},
}
);

View File

@@ -0,0 +1,33 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads a persistent value using dget.
*/
export const dgetNode = createNodeModule(
{
id: "data_dget",
title: "Data Get",
category: "Data",
description: "Retrieve a persistent number using DGET().",
searchTags: ["dget", "load", "data", "persistent"],
inputs: [
{
id: "index",
name: "Index",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const index = resolveValueInput("index", "0");
return `dget(${index})`;
},
}
);

View File

@@ -0,0 +1,43 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Writes a persistent value using dset.
*/
export const dsetNode = createNodeModule(
{
id: "data_dset",
title: "Data Set",
category: "Data",
description: "Store a persistent number using DSET().",
searchTags: ["dset", "save", "data", "persistent"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "index",
name: "Index",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "value",
name: "Value",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const index = resolveValueInput("index", "0");
const value = resolveValueInput("value", "0");
const line = `${indent(indentLevel)}dset(${index}, ${value})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,18 @@
import { cartdataNode } from "./cartdata.js";
import { dsetNode } from "./dset.js";
import { dgetNode } from "./dget.js";
/**
* All data persistence node modules backed by PICO-8 helpers.
*/
export const dataNodes = [cartdataNode, dsetNode, dgetNode];
/**
* Checklist tracking coverage of documented cart data helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const dataFunctionChecklist = [
{ function: "CARTDATA", nodeId: "data_cartdata", implemented: true },
{ function: "DSET", nodeId: "data_dset", implemented: true },
{ function: "DGET", nodeId: "data_dget", implemented: true },
];

View File

@@ -0,0 +1,35 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Applies host configuration flags via devkit_config().
*/
export const devkitConfigNode = createNodeModule(
{
id: "devkit_config",
title: "Devkit Config",
category: "Devkit",
description: "Send a configuration string to devkit_config().",
searchTags: ["devkit", "config", "host"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "config",
name: "Config",
direction: "input",
kind: "string",
defaultValue: "",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const config = resolveValueInput("config", '""');
const line = `${indent(indentLevel)}devkit_config(${config})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,33 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads values from devkit-provided inputs via devkit_input().
*/
export const devkitInputReadNode = createNodeModule(
{
id: "devkit_input_read",
title: "Devkit Input Read",
category: "Devkit",
description: "Fetch the value of a devkit input index.",
searchTags: ["devkit", "input", "read"],
inputs: [
{
id: "index",
name: "Index",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "any" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const index = resolveValueInput("index", "0");
return `devkit_input(${index})`;
},
}
);

View File

@@ -0,0 +1,43 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Sends values to devkit inputs via devkit_input().
*/
export const devkitInputWriteNode = createNodeModule(
{
id: "devkit_input_write",
title: "Devkit Input Write",
category: "Devkit",
description: "Push a value to a devkit input index.",
searchTags: ["devkit", "input", "write"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "index",
name: "Index",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "value",
name: "Value",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const index = resolveValueInput("index", "0");
const value = resolveValueInput("value", "0");
const line = `${indent(indentLevel)}devkit_input(${index}, ${value})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,22 @@
import { devkitConfigNode } from "./devkitConfig.js";
import { devkitInputReadNode } from "./devkitInputRead.js";
import { devkitInputWriteNode } from "./devkitInputWrite.js";
/**
* Node modules covering devkit helpers.
*/
export const devkitNodes = [
devkitConfigNode,
devkitInputReadNode,
devkitInputWriteNode,
];
/**
* Checklist tracking coverage of devkit helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const devkitFunctionChecklist = [
{ function: "DEVKIT_CONFIG", nodeId: "devkit_config", implemented: true },
{ function: "DEVKIT_INPUT (read)", nodeId: "devkit_input_read", implemented: true },
{ function: "DEVKIT_INPUT (write)", nodeId: "devkit_input_write", implemented: true },
];

View File

@@ -0,0 +1,26 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Lifecycle event triggered once per visible frame.
* @type {import('../nodeTypes.js').NodeModule}
*/
export const eventDrawNode = createNodeModule(
{
id: "event_draw",
title: "Event Draw",
category: "Events",
description: "Called once per visible frame.",
searchTags: ["draw", "render", "frame", "loop"],
unique: true,
inputs: [],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
isEntryPoint: true,
eventName: "_draw",
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
}
);

View File

@@ -0,0 +1,26 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Lifecycle entry triggered once when the cart boots.
* @type {import('../nodeTypes.js').NodeModule}
*/
export const eventInitNode = createNodeModule(
{
id: "event_start",
title: "Event Init",
category: "Events",
description: "Called once on cart startup.",
searchTags: ["start", "init", "boot", "lifecycle"],
unique: true,
inputs: [],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
isEntryPoint: true,
eventName: "_init",
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* @type {import('../nodeTypes.js').NodeModule}
*/
export const eventUpdateNode = createNodeModule(
{
id: "event_update",
title: "Event Update",
category: "Events",
description: "Called once per update",
searchTags: ["update", "loop", "frame", "tick"],
unique: true,
inputs: [],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
isEntryPoint: true,
eventName: "_update",
emitExec: ({ emitNextExec }) => emitNextExec("exec_out"),
}
);

View File

@@ -0,0 +1,92 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Iterates from start to end inclusive using PICO-8 for semantics.
*/
export const forLoopNode = createNodeModule(
{
id: "for_loop",
title: "For Loop",
category: "Logic",
description:
"Iterate from start to end inclusive using PICO-8 for semantics.",
searchTags: ["loop", "for", "iterate", "counter"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "start",
name: "Start",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "end",
name: "End",
direction: "input",
kind: "number",
defaultValue: 10,
},
{
id: "step",
name: "Step",
direction: "input",
kind: "number",
defaultValue: 1,
},
],
outputs: [
{ id: "loop", name: "Loop", direction: "output", kind: "exec" },
{ id: "completed", name: "Completed", direction: "output", kind: "exec" },
],
properties: [
{
key: "index",
label: "Index Variable",
type: "string",
defaultValue: "i",
},
],
},
{
emitExec: ({
node,
indent,
indentLevel,
resolveValueInput,
sanitizeIdentifier,
emitBranch,
path,
}) => {
const index = sanitizeIdentifier(String(node.properties.index ?? "i"));
const startValue = resolveValueInput("start", "0");
const endValue = resolveValueInput("end", "0");
const stepValue = resolveValueInput("step", "1");
const lines = [
`${indent(
indentLevel
)}for ${index} = ${startValue}, ${endValue}, ${stepValue} do`,
];
const loopLines = emitBranch("loop", {
indentLevel: indentLevel + 1,
path: new Set(path),
});
if (!loopLines.length) {
lines.push(`${indent(indentLevel + 1)}-- loop body`);
} else {
lines.push(...loopLines);
}
lines.push(`${indent(indentLevel)}end`);
lines.push(
...emitBranch("completed", {
indentLevel,
path,
})
);
return lines;
},
}
);

View File

@@ -0,0 +1,30 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Exposes the current value of a variable.
*/
export const getVariableNode = createNodeModule(
{
id: "get_var",
title: "Get Variable",
category: "Logic",
description: "Expose the current value of a variable.",
searchTags: ["get", "read", "variable", "access"],
inputs: [],
outputs: [{ id: "value", name: "Value", direction: "output", kind: "any" }],
properties: [
{
key: "name",
label: "Variable Name",
type: "string",
defaultValue: "score",
},
],
},
{
evaluateValue: ({ node, sanitizeIdentifier }) => {
const name = sanitizeIdentifier(String(node.properties.name ?? "var"));
return name;
},
}
);

View File

@@ -0,0 +1,33 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads a value from a GPIO pin using gpio().
*/
export const gpioReadNode = createNodeModule(
{
id: "gpio_read",
title: "GPIO Read",
category: "IO",
description: "Fetch the current value of a GPIO pin.",
searchTags: ["gpio", "hardware", "input"],
inputs: [
{
id: "index",
name: "Pin",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const pin = resolveValueInput("index", "0");
return `gpio(${pin})`;
},
}
);

View File

@@ -0,0 +1,43 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Writes a value to a GPIO pin using gpio().
*/
export const gpioWriteNode = createNodeModule(
{
id: "gpio_write",
title: "GPIO Write",
category: "IO",
description: "Set the value for a GPIO pin.",
searchTags: ["gpio", "hardware", "output"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "index",
name: "Pin",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "value",
name: "Value",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const pin = resolveValueInput("index", "0");
const value = resolveValueInput("value", "0");
const line = `${indent(indentLevel)}gpio(${pin}, ${value})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,16 @@
import { gpioReadNode } from "./gpioRead.js";
import { gpioWriteNode } from "./gpioWrite.js";
/**
* Node modules covering GPIO helpers.
*/
export const gpioNodes = [gpioReadNode, gpioWriteNode];
/**
* Checklist tracking coverage of GPIO read/write helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const gpioFunctionChecklist = [
{ function: "GPIO (read)", nodeId: "gpio_read", implemented: true },
{ function: "GPIO (write)", nodeId: "gpio_write", implemented: true },
];

View File

@@ -0,0 +1,37 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Applies a camera offset to subsequent draw calls.
*/
export const cameraNode = createNodeModule(
{
id: "graphics_camera",
title: "Add Camera Offset",
category: "Graphics",
description: "Shift the draw origin by the specified camera offset.",
searchTags: ["camera", "offset", "scroll", "view"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "x", name: "X", direction: "input", kind: "number" },
{ id: "y", name: "Y", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const x = resolveValueInput("x", OMIT);
const y = resolveValueInput("y", OMIT);
let call = "camera()";
if (x !== OMIT) {
call = `camera(${x}, ${y === OMIT ? "0" : y})`;
}
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,57 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws an outlined circle.
*/
export const circNode = createNodeModule(
{
id: "graphics_circ",
title: "Draw Circle",
category: "Graphics",
description: "Render an outlined circle at the specified position.",
searchTags: ["circle", "outline", "shape", "draw"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "radius",
name: "Radius",
direction: "input",
kind: "number",
defaultValue: 4,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const radius = resolveValueInput("radius", "4");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x, y, radius];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}circ(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,57 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a filled circle.
*/
export const circfillNode = createNodeModule(
{
id: "graphics_circfill",
title: "Draw Filled Circle",
category: "Graphics",
description: "Render a filled circle at the specified position.",
searchTags: ["circle", "filled", "shape", "draw"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "radius",
name: "Radius",
direction: "input",
kind: "number",
defaultValue: 4,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const radius = resolveValueInput("radius", "4");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x, y, radius];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}circfill(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,63 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Adjusts the active clipping rectangle for subsequent draw calls.
*/
export const clipNode = createNodeModule(
{
id: "graphics_clip",
title: "Set Clipping Rectangle",
category: "Graphics",
description:
"Set or reset the clipping rectangle that constrains all draw operations.",
searchTags: ["clip", "clipping", "graphics", "viewport"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "x", name: "X", direction: "input", kind: "number" },
{ id: "y", name: "Y", direction: "input", kind: "number" },
{ id: "w", name: "Width", direction: "input", kind: "number" },
{ id: "h", name: "Height", direction: "input", kind: "number" },
{
id: "clip_previous",
name: "Clip Previous",
direction: "input",
kind: "boolean",
defaultValue: false,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const x = resolveValueInput("x", OMIT);
const y = resolveValueInput("y", OMIT);
const w = resolveValueInput("w", OMIT);
const h = resolveValueInput("h", OMIT);
const clipPrevious = resolveValueInput("clip_previous", OMIT);
let call;
if (x === OMIT && y === OMIT && w === OMIT && h === OMIT) {
call = "clip()";
} else {
const args = [
x === OMIT ? "0" : x,
y === OMIT ? "0" : y,
w === OMIT ? "128" : w,
h === OMIT ? "128" : h,
];
if (clipPrevious !== OMIT) {
args.push(clipPrevious);
}
call = `clip(${args.join(", ")})`;
}
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,32 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Clears the screen and resets clipping.
*/
export const clsNode = createNodeModule(
{
id: "graphics_cls",
title: "Clear Screen",
category: "Graphics",
description: "Clear the display, optionally filling with a specific color.",
searchTags: ["cls", "clear", "screen", "background"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const color = resolveValueInput("color", OMIT);
const call = color === OMIT ? "cls()" : `cls(${color})`;
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,32 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Updates the current draw color.
*/
export const colorNode = createNodeModule(
{
id: "graphics_color",
title: "Set Active Color",
category: "Graphics",
description: "Set the active draw color used by subsequent graphics calls.",
searchTags: ["color", "color", "ink", "draw"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const color = resolveValueInput("color", OMIT);
const call = color === OMIT ? "color()" : `color(${color})`;
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,46 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Positions the print cursor and optionally sets the draw color.
*/
export const cursorNode = createNodeModule(
{
id: "graphics_cursor",
title: "Set Cursor",
category: "Graphics",
description:
"Move the print cursor and optionally change the active color.",
searchTags: ["cursor", "print", "text", "position"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "x", name: "X", direction: "input", kind: "number" },
{ id: "y", name: "Y", direction: "input", kind: "number" },
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const x = resolveValueInput("x", OMIT);
const y = resolveValueInput("y", OMIT);
const color = resolveValueInput("color", OMIT);
let call = "cursor()";
if (x !== OMIT) {
const args = [x, y === OMIT ? "0" : y];
if (color !== OMIT) {
args.push(color);
}
call = `cursor(${args.join(", ")})`;
}
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,39 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads sprite flag data.
*/
export const fgetNode = createNodeModule(
{
id: "graphics_fget",
title: "Get Sprite Flag",
category: "Graphics",
description: "Retrieve the bitfield or specific flag value for a sprite.",
searchTags: ["fget", "flag", "sprite", "meta"],
inputs: [
{
id: "sprite",
name: "Sprite",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "flag", name: "Flag", direction: "input", kind: "number" },
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const OMIT = "__pg_omit__";
const sprite = resolveValueInput("sprite", "0");
const flag = resolveValueInput("flag", OMIT);
if (flag === OMIT) {
return `fget(${sprite})`;
}
return `fget(${sprite}, ${flag})`;
},
}
);

View File

@@ -0,0 +1,31 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Configures the active fill pattern.
*/
export const fillpNode = createNodeModule(
{
id: "graphics_fillp",
title: "Set Fill Pattern",
category: "Graphics",
description: "Set the 4x4 fill pattern bitfield used by many draw calls.",
searchTags: ["fillp", "pattern", "fill", "hatch"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "pattern", name: "Pattern", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const pattern = resolveValueInput("pattern", "__pg_omit__");
const call = pattern === "__pg_omit__" ? "fillp()" : `fillp(${pattern})`;
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,52 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Updates sprite flag values.
*/
export const fsetNode = createNodeModule(
{
id: "graphics_fset",
title: "Set Sprite Flag",
category: "Graphics",
description: "Set a sprite's flag bitfield or a specific flag value.",
searchTags: ["fset", "flag", "sprite", "meta"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "sprite",
name: "Sprite",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "flag", name: "Flag", direction: "input", kind: "number" },
{
id: "value",
name: "Value",
direction: "input",
kind: "any",
defaultValue: true,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const sprite = resolveValueInput("sprite", "0");
const flag = resolveValueInput("flag", OMIT);
const value = resolveValueInput("value", "false");
const args = [sprite];
if (flag === OMIT) {
args.push(value);
} else {
args.push(flag, value);
}
const line = `${indent(indentLevel)}fset(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,88 @@
import { clipNode } from "./clip.js";
import { psetNode } from "./pset.js";
import { pgetNode } from "./pget.js";
import { sgetNode } from "./sget.js";
import { ssetNode } from "./sset.js";
import { fgetNode } from "./fget.js";
import { fsetNode } from "./fset.js";
import { cursorNode } from "./cursor.js";
import { colorNode } from "./color.js";
import { clsNode } from "./cls.js";
import { cameraNode } from "./camera.js";
import { circNode } from "./circ.js";
import { circfillNode } from "./circfill.js";
import { ovalNode } from "./oval.js";
import { ovalfillNode } from "./ovalfill.js";
import { lineNode } from "./line.js";
import { rectNode } from "./rect.js";
import { rectfillNode } from "./rectfill.js";
import { rrectNode } from "./rrect.js";
import { rrectfillNode } from "./rrectfill.js";
import { palNode } from "./pal.js";
import { paltNode } from "./palt.js";
import { sprNode } from "./spr.js";
import { ssprNode } from "./sspr.js";
import { fillpNode } from "./fillp.js";
/**
* All graphics related node modules backed by PICO-8 draw helpers.
*/
export const graphicsNodes = [
clipNode,
psetNode,
pgetNode,
sgetNode,
ssetNode,
fgetNode,
fsetNode,
cursorNode,
colorNode,
clsNode,
cameraNode,
circNode,
circfillNode,
ovalNode,
ovalfillNode,
lineNode,
rectNode,
rectfillNode,
rrectNode,
rrectfillNode,
palNode,
paltNode,
sprNode,
ssprNode,
fillpNode,
];
/**
* Checklist tracking coverage of documented graphics helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const graphicsFunctionChecklist = [
{ function: "CLIP", nodeId: "graphics_clip", implemented: true },
{ function: "PSET", nodeId: "graphics_pset", implemented: true },
{ function: "PGET", nodeId: "graphics_pget", implemented: true },
{ function: "SGET", nodeId: "graphics_sget", implemented: true },
{ function: "SSET", nodeId: "graphics_sset", implemented: true },
{ function: "FGET", nodeId: "graphics_fget", implemented: true },
{ function: "FSET", nodeId: "graphics_fset", implemented: true },
{ function: "CURSOR", nodeId: "graphics_cursor", implemented: true },
{ function: "COLOR", nodeId: "graphics_color", implemented: true },
{ function: "CLS", nodeId: "graphics_cls", implemented: true },
{ function: "CAMERA", nodeId: "graphics_camera", implemented: true },
{ function: "CIRC", nodeId: "graphics_circ", implemented: true },
{ function: "CIRCFILL", nodeId: "graphics_circfill", implemented: true },
{ function: "OVAL", nodeId: "graphics_oval", implemented: true },
{ function: "OVALFILL", nodeId: "graphics_ovalfill", implemented: true },
{ function: "LINE", nodeId: "graphics_line", implemented: true },
{ function: "RECT", nodeId: "graphics_rect", implemented: true },
{ function: "RECTFILL", nodeId: "graphics_rectfill", implemented: true },
{ function: "RRECT", nodeId: "graphics_rrect", implemented: true },
{ function: "RRECTFILL", nodeId: "graphics_rrectfill", implemented: true },
{ function: "PAL", nodeId: "graphics_pal", implemented: true },
{ function: "PALT", nodeId: "graphics_palt", implemented: true },
{ function: "SPR", nodeId: "graphics_spr", implemented: true },
{ function: "SSPR", nodeId: "graphics_sspr", implemented: true },
{ function: "FILLP", nodeId: "graphics_fillp", implemented: true },
];

View File

@@ -0,0 +1,60 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a line segment.
*/
export const lineNode = createNodeModule(
{
id: "graphics_line",
title: "Draw Line",
category: "Graphics",
description:
"Draw a line between two points or update the current pen position.",
searchTags: ["line", "draw", "segment", "stroke"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "x1", name: "X1", direction: "input", kind: "number" },
{ id: "y1", name: "Y1", direction: "input", kind: "number" },
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const x0 = resolveValueInput("x0", "0");
const y0 = resolveValueInput("y0", "0");
const x1 = resolveValueInput("x1", OMIT);
const y1 = resolveValueInput("y1", OMIT);
const color = resolveValueInput("color", OMIT);
const args = [x0, y0];
if (x1 !== OMIT && y1 !== OMIT) {
args.push(x1, y1);
if (color !== OMIT) {
args.push(color);
}
}
const line = `${indent(indentLevel)}line(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,65 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws an outlined oval.
*/
export const ovalNode = createNodeModule(
{
id: "graphics_oval",
title: "Draw Oval",
category: "Graphics",
description: "Render an ellipse using the provided bounding box.",
searchTags: ["oval", "ellipse", "shape", "outline"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "x1",
name: "X1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "y1",
name: "Y1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x0 = resolveValueInput("x0", "0");
const y0 = resolveValueInput("y0", "0");
const x1 = resolveValueInput("x1", "8");
const y1 = resolveValueInput("y1", "8");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x0, y0, x1, y1];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}oval(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,65 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a filled oval.
*/
export const ovalfillNode = createNodeModule(
{
id: "graphics_ovalfill",
title: "Draw Filled Oval",
category: "Graphics",
description: "Render a filled ellipse using the provided bounding box.",
searchTags: ["oval", "ellipse", "filled", "shape"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "x1",
name: "X1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "y1",
name: "Y1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x0 = resolveValueInput("x0", "0");
const y0 = resolveValueInput("y0", "0");
const x1 = resolveValueInput("x1", "8");
const y1 = resolveValueInput("y1", "8");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x0, y0, x1, y1];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}ovalfill(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,51 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Remaps palette colors.
*/
export const palNode = createNodeModule(
{
id: "graphics_pal",
title: "Remap Palette Color",
category: "Graphics",
description: "Swap one palette color for another or reset the palette.",
searchTags: ["pal", "palette", "color", "remap"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "c0", name: "Color 0", direction: "input", kind: "number" },
{
id: "c1",
name: "Color 1",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "p", name: "Palette", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const c0 = resolveValueInput("c0", OMIT);
const c1 = resolveValueInput("c1", "0");
const p = resolveValueInput("p", OMIT);
let call = "pal()";
if (c0 !== OMIT) {
const args = [c0, c1];
if (p !== OMIT) {
args.push(p);
}
call = `pal(${args.join(", ")})`;
}
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,47 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Adjusts sprite transparency settings.
*/
export const paltNode = createNodeModule(
{
id: "graphics_palt",
title: "Set Sprite Transparency",
category: "Graphics",
description: "Set or reset sprite transparency flags.",
searchTags: ["palt", "transparency", "palette", "sprite"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "color", name: "Color", direction: "input", kind: "number" },
{
id: "transparent",
name: "Transparent",
direction: "input",
kind: "boolean",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const color = resolveValueInput("color", OMIT);
const transparent = resolveValueInput("transparent", OMIT);
let call = "palt()";
if (color !== OMIT) {
if (transparent === OMIT) {
call = `palt(${color})`;
} else {
call = `palt(${color}, ${transparent})`;
}
}
const lines = [`${indent(indentLevel)}${call}`];
lines.push(...emitNextExec("exec_out"));
return lines;
},
}
);

View File

@@ -0,0 +1,41 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads the color of a screen pixel.
*/
export const pgetNode = createNodeModule(
{
id: "graphics_pget",
title: "Get Pixel Color",
category: "Graphics",
description: "Fetch the color index at the given screen coordinate.",
searchTags: ["pget", "pixel", "read", "sample"],
inputs: [
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Color", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
return `pget(${x}, ${y})`;
},
}
);

View File

@@ -0,0 +1,50 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a single pixel using the current or specified color.
*/
export const psetNode = createNodeModule(
{
id: "graphics_pset",
title: "Set Pixel Color",
category: "Graphics",
description: "Set a pixel on screen to a color index.",
searchTags: ["pset", "pixel", "draw", "plot"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const color = resolveValueInput("color", OMIT);
const args = [x, y];
if (color !== OMIT) {
args.push(color);
}
const line = `${indent(indentLevel)}pset(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,65 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws an outlined rectangle.
*/
export const rectNode = createNodeModule(
{
id: "graphics_rect",
title: "Draw Rectangle",
category: "Graphics",
description: "Render a rectangle outline defined by two corners.",
searchTags: ["rect", "rectangle", "outline", "shape"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "x1",
name: "X1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "y1",
name: "Y1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x0 = resolveValueInput("x0", "0");
const y0 = resolveValueInput("y0", "0");
const x1 = resolveValueInput("x1", "8");
const y1 = resolveValueInput("y1", "8");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x0, y0, x1, y1];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}rect(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,65 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a filled rectangle.
*/
export const rectfillNode = createNodeModule(
{
id: "graphics_rectfill",
title: "Draw Filled Rectangle",
category: "Graphics",
description: "Render a filled rectangle defined by two corners.",
searchTags: ["rect", "rectangle", "filled", "shape"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "x1",
name: "X1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "y1",
name: "Y1",
direction: "input",
kind: "number",
defaultValue: 8,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x0 = resolveValueInput("x0", "0");
const y0 = resolveValueInput("y0", "0");
const x1 = resolveValueInput("x1", "8");
const y1 = resolveValueInput("y1", "8");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x0, y0, x1, y1];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}rectfill(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,73 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws an outlined rounded rectangle.
*/
export const rrectNode = createNodeModule(
{
id: "graphics_rrect",
title: "Draw Rounded Rectangle",
category: "Graphics",
description: "Render a rounded rectangle outline with the supplied radius.",
searchTags: ["rounded", "rectangle", "outline", "shape"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "w",
name: "Width",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "h",
name: "Height",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "r",
name: "Radius",
direction: "input",
kind: "number",
defaultValue: 2,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const w = resolveValueInput("w", "8");
const h = resolveValueInput("h", "8");
const r = resolveValueInput("r", "2");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x, y, w, h, r];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}rrect(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,73 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a filled rounded rectangle.
*/
export const rrectfillNode = createNodeModule(
{
id: "graphics_rrectfill",
title: "Draw Filled Rounded Rectangle",
category: "Graphics",
description: "Render a filled rounded rectangle with the supplied radius.",
searchTags: ["rounded", "rectangle", "filled", "shape"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "w",
name: "Width",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "h",
name: "Height",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "r",
name: "Radius",
direction: "input",
kind: "number",
defaultValue: 2,
},
{ id: "color", name: "Color", direction: "input", kind: "number" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const w = resolveValueInput("w", "8");
const h = resolveValueInput("h", "8");
const r = resolveValueInput("r", "2");
const color = resolveValueInput("color", "__pg_omit__");
const args = [x, y, w, h, r];
if (color !== "__pg_omit__") {
args.push(color);
}
const line = `${indent(indentLevel)}rrectfill(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,42 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads a pixel from the sprite sheet.
*/
export const sgetNode = createNodeModule(
{
id: "graphics_sget",
title: "Get Sprite Pixel",
category: "Graphics",
description:
"Retrieve the color index from the sprite sheet at the given coordinate.",
searchTags: ["sget", "sprite", "sheet", "read"],
inputs: [
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Color", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
return `sget(${x}, ${y})`;
},
}
);

View File

@@ -0,0 +1,76 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws sprites from the sprite sheet.
*/
export const sprNode = createNodeModule(
{
id: "graphics_spr",
title: "Draw Sprite",
category: "Graphics",
description: "Blit one or more sprites at the given screen position.",
searchTags: ["spr", "sprite", "draw", "blit"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "sprite",
name: "Sprite",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "w", name: "Width", direction: "input", kind: "number" },
{ id: "h", name: "Height", direction: "input", kind: "number" },
{ id: "flip_x", name: "Flip X", direction: "input", kind: "boolean" },
{ id: "flip_y", name: "Flip Y", direction: "input", kind: "boolean" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const sprite = resolveValueInput("sprite", "0");
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const w = resolveValueInput("w", OMIT);
const h = resolveValueInput("h", OMIT);
const flipX = resolveValueInput("flip_x", OMIT);
const flipY = resolveValueInput("flip_y", OMIT);
const args = [sprite, x, y];
const includeSize =
w !== OMIT || h !== OMIT || flipX !== OMIT || flipY !== OMIT;
if (includeSize) {
args.push(w === OMIT ? "1" : w, h === OMIT ? "1" : h);
}
if (flipX !== OMIT || flipY !== OMIT) {
args.push(
flipX === OMIT ? "false" : flipX,
flipY === OMIT ? "false" : flipY
);
}
const line = `${indent(indentLevel)}spr(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,51 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Writes a pixel to the sprite sheet.
*/
export const ssetNode = createNodeModule(
{
id: "graphics_sset",
title: "Set Sprite Pixel",
category: "Graphics",
description: "Assign a color to the sprite sheet at the given coordinate.",
searchTags: ["sset", "sprite", "sheet", "write"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "color",
name: "Color",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const color = resolveValueInput("color", "0");
const line = `${indent(indentLevel)}sset(${x}, ${y}, ${color})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,100 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws a rectangular region of the sprite sheet with scaling support.
*/
export const ssprNode = createNodeModule(
{
id: "graphics_sspr",
title: "Draw Scaled Sprite",
category: "Graphics",
description: "Stretch or flip a sprite sheet region to the screen.",
searchTags: ["sspr", "sprite", "scale", "blit"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "sx",
name: "SX",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "sy",
name: "SY",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "sw",
name: "SW",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "sh",
name: "SH",
direction: "input",
kind: "number",
defaultValue: 8,
},
{
id: "dx",
name: "DX",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "dy",
name: "DY",
direction: "input",
kind: "number",
defaultValue: 0,
},
{ id: "dw", name: "DW", direction: "input", kind: "number" },
{ id: "dh", name: "DH", direction: "input", kind: "number" },
{ id: "flip_x", name: "Flip X", direction: "input", kind: "boolean" },
{ id: "flip_y", name: "Flip Y", direction: "input", kind: "boolean" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const sx = resolveValueInput("sx", "0");
const sy = resolveValueInput("sy", "0");
const sw = resolveValueInput("sw", "8");
const sh = resolveValueInput("sh", "8");
const dx = resolveValueInput("dx", "0");
const dy = resolveValueInput("dy", "0");
const dw = resolveValueInput("dw", OMIT);
const dh = resolveValueInput("dh", OMIT);
const flipX = resolveValueInput("flip_x", OMIT);
const flipY = resolveValueInput("flip_y", OMIT);
const args = [sx, sy, sw, sh, dx, dy];
const includeDestSize =
dw !== OMIT || dh !== OMIT || flipX !== OMIT || flipY !== OMIT;
if (includeDestSize) {
args.push(dw === OMIT ? sw : dw, dh === OMIT ? sh : dh);
}
if (flipX !== OMIT || flipY !== OMIT) {
args.push(
flipX === OMIT ? "false" : flipX,
flipY === OMIT ? "false" : flipY
);
}
const line = `${indent(indentLevel)}sspr(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,67 @@
import { createNodeModule } from "../nodeTypes.js";
/**
* Branches execution based on a boolean condition.
*/
export const ifNode = createNodeModule(
{
id: "if",
title: "If",
category: "Logic",
description: "Branch execution based on a boolean condition.",
searchTags: ["if", "condition", "branch", "logic"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "condition",
name: "Condition",
direction: "input",
kind: "boolean",
},
],
outputs: [
{ id: "then", name: "Then", direction: "output", kind: "exec" },
{ id: "else", name: "Else", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({
indent,
indentLevel,
resolveValueInput,
emitBranch,
findExecTargets,
path,
}) => {
const condition = resolveValueInput("condition", "false");
const lines = [`${indent(indentLevel)}if ${condition} then`];
const thenLines = emitBranch("then", {
indentLevel: indentLevel + 1,
path: new Set(path),
});
if (!thenLines.length) {
lines.push(`${indent(indentLevel + 1)}-- then branch`);
} else {
lines.push(...thenLines);
}
if (findExecTargets("else").length) {
lines.push(`${indent(indentLevel)}else`);
const elseLines = emitBranch("else", {
indentLevel: indentLevel + 1,
path: new Set(path),
});
if (!elseLines.length) {
lines.push(`${indent(indentLevel + 1)}-- else branch`);
} else {
lines.push(...elseLines);
}
}
lines.push(`${indent(indentLevel)}end`);
return lines;
},
}
);

View File

@@ -0,0 +1,87 @@
import { eventInitNode } from "./eventStart.js";
import { eventUpdateNode } from "./eventUpdate.js";
import { eventDrawNode } from "./eventDraw.js";
import { printNode } from "./print.js";
import { setVariableNode } from "./setVariable.js";
import { getVariableNode } from "./getVariable.js";
import { numberLiteralNode } from "./numberLiteral.js";
import { stringLiteralNode } from "./stringLiteral.js";
import { booleanLiteralNode } from "./booleanLiteral.js";
import { addNumberNode } from "./addNumber.js";
import { multiplyNumberNode } from "./multiplyNumber.js";
import { compareNode } from "./compare.js";
import { sequenceNode } from "./sequence.js";
import { ifNode } from "./ifNode.js";
import { forLoopNode } from "./forLoop.js";
import { customEventNode } from "./customEvent.js";
import { callCustomEventNode } from "./callCustomEvent.js";
import { graphicsNodes, graphicsFunctionChecklist } from "./graphics/index.js";
import { systemNodes, systemFunctionChecklist } from "./system/index.js";
import { tableNodes, tableFunctionChecklist } from "./table/index.js";
import { inputNodes, inputFunctionChecklist } from "./input/index.js";
import { audioNodes, audioFunctionChecklist } from "./audio/index.js";
import { mapNodes, mapFunctionChecklist } from "./map/index.js";
import { memoryNodes, memoryFunctionChecklist } from "./memory/index.js";
import { mathNodes, mathFunctionChecklist } from "./math/index.js";
import { menuNodes, menuFunctionChecklist } from "./menu/index.js";
import { stringNodes, stringFunctionChecklist } from "./strings/index.js";
import { dataNodes, dataFunctionChecklist } from "./data/index.js";
import { gpioNodes, gpioFunctionChecklist } from "./gpio/index.js";
import { serialNodes, serialFunctionChecklist } from "./serial/index.js";
import { devkitNodes, devkitFunctionChecklist } from "./devkit/index.js";
import { luaNodes, luaFunctionChecklist } from "./lua/index.js";
import { localVariableNodes } from "./localVariables.js";
export const nodeModules = [
eventInitNode,
eventUpdateNode,
eventDrawNode,
printNode,
setVariableNode,
getVariableNode,
numberLiteralNode,
stringLiteralNode,
booleanLiteralNode,
addNumberNode,
multiplyNumberNode,
compareNode,
sequenceNode,
ifNode,
forLoopNode,
customEventNode,
callCustomEventNode,
...localVariableNodes,
...graphicsNodes,
...systemNodes,
...tableNodes,
...inputNodes,
...audioNodes,
...mapNodes,
...memoryNodes,
...mathNodes,
...menuNodes,
...stringNodes,
...dataNodes,
...gpioNodes,
...serialNodes,
...devkitNodes,
...luaNodes,
];
export {
systemFunctionChecklist,
graphicsFunctionChecklist,
tableFunctionChecklist,
inputFunctionChecklist,
audioFunctionChecklist,
mapFunctionChecklist,
memoryFunctionChecklist,
mathFunctionChecklist,
menuFunctionChecklist,
stringFunctionChecklist,
dataFunctionChecklist,
gpioFunctionChecklist,
serialFunctionChecklist,
devkitFunctionChecklist,
luaFunctionChecklist,
};

View File

@@ -0,0 +1,55 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads the state of a PICO-8 button using BTN.
*/
export const btnNode = createNodeModule(
{
id: "input_btn",
title: "Button State",
category: "Input",
description: "Read the current pressed state for a controller button.",
searchTags: ["btn", "input", "button", "press"],
inputs: [
{
id: "button",
name: "Button",
direction: "input",
kind: "number",
description: "Button index or glyph code",
},
{
id: "player",
name: "Player",
direction: "input",
kind: "number",
description: "Optional player index",
},
],
outputs: [
{ id: "value", name: "Pressed", direction: "output", kind: "boolean" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const OMIT = "__pg_omit__";
const button = resolveValueInput("button", OMIT);
const player = resolveValueInput("player", OMIT);
if (button === OMIT && player === OMIT) {
return "btn()";
}
if (button === OMIT) {
return `btn(nil, ${player})`;
}
if (player === OMIT) {
return `btn(${button})`;
}
return `btn(${button}, ${player})`;
},
}
);

View File

@@ -0,0 +1,56 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads button press transitions using BTNP.
*/
export const btnpNode = createNodeModule(
{
id: "input_btnp",
title: "Button Press",
category: "Input",
description:
"Detect when a button is initially pressed or repeats using BTNP.",
searchTags: ["btnp", "input", "button", "press", "repeat"],
inputs: [
{
id: "button",
name: "Button",
direction: "input",
kind: "number",
description: "Button index or glyph code",
},
{
id: "player",
name: "Player",
direction: "input",
kind: "number",
description: "Optional player index",
},
],
outputs: [
{ id: "value", name: "Pressed", direction: "output", kind: "boolean" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const OMIT = "__pg_omit__";
const button = resolveValueInput("button", OMIT);
const player = resolveValueInput("player", OMIT);
if (button === OMIT && player === OMIT) {
return "btnp()";
}
if (button === OMIT) {
return `btnp(nil, ${player})`;
}
if (player === OMIT) {
return `btnp(${button})`;
}
return `btnp(${button}, ${player})`;
},
}
);

View File

@@ -0,0 +1,175 @@
import { createNodeModule } from "../../nodeTypes.js";
const DEFAULT_PLAYER_VALUE = "0";
/**
* Normalizes the stored player property to a valid controller index.
*
* @param {unknown} value Raw inspector value.
* @returns {number}
*/
const resolvePlayerIndex = (value) => {
const numeric = Number.parseInt(
typeof value === "string" ? value : String(value ?? DEFAULT_PLAYER_VALUE),
10
);
if (!Number.isFinite(numeric) || numeric < 0) {
return 0;
}
if (numeric > 7) {
return 7;
}
return numeric;
};
const BUTTON_OPTIONS = [
{ value: "0", label: "Left" },
{ value: "1", label: "Right" },
{ value: "2", label: "Up" },
{ value: "3", label: "Down" },
{ value: "4", label: "O" },
{ value: "5", label: "X" },
];
const BUTTON_VALUE_SET = new Set(BUTTON_OPTIONS.map((option) => option.value));
const DEFAULT_BUTTON_VALUE = BUTTON_OPTIONS[0]?.value ?? "0";
/**
* Normalizes button property values to a valid BTN index represented as a string.
*
* @param {unknown} value Raw inspector value.
* @returns {string}
*/
const resolveButtonValue = (value) => {
const numeric = Number.parseInt(
typeof value === "string" ? value : String(value ?? DEFAULT_BUTTON_VALUE),
10
);
const candidate = Number.isFinite(numeric) ? String(numeric) : DEFAULT_BUTTON_VALUE;
if (BUTTON_VALUE_SET.has(candidate)) {
return candidate;
}
return DEFAULT_BUTTON_VALUE;
};
const PLAYER_OPTIONS = [
{ value: "0", label: "Player 0 (Cursors / Z X)" },
{ value: "1", label: "Player 1 (SFED / Tab Q W A)" },
{ value: "2", label: "Player 2" },
{ value: "3", label: "Player 3" },
{ value: "4", label: "Player 4" },
{ value: "5", label: "Player 5" },
{ value: "6", label: "Player 6" },
{ value: "7", label: "Player 7" },
];
/**
* Evaluates whether the configured button is currently pressed.
*/
export const controllerButtonsNode = createNodeModule(
{
id: "input_button_constants",
title: "Named Button",
category: "Input",
description:
"Read a specific controller button with a dropdown selector and optional player override.",
searchTags: [
"button",
"controller",
"dpad",
"input",
"player",
"named",
"press",
],
inputs: [
{
id: "button",
name: "Button",
direction: "input",
kind: "number",
description: "Optional button index override",
},
{
id: "player",
name: "Player",
direction: "input",
kind: "number",
description: "Optional player index override",
},
],
outputs: [
{
id: "pressed",
name: "Pressed",
direction: "output",
kind: "boolean",
},
],
properties: [
{
key: "button",
label: "Button",
type: "enum",
defaultValue: DEFAULT_BUTTON_VALUE,
options: BUTTON_OPTIONS,
},
{
key: "player",
label: "Default Player",
type: "enum",
defaultValue: DEFAULT_PLAYER_VALUE,
options: PLAYER_OPTIONS,
},
],
initializeProperties: (properties) => {
properties.button = resolveButtonValue(properties.button);
const normalizedPlayer = resolvePlayerIndex(properties.player);
properties.player = String(normalizedPlayer);
const buttonInlineKey = "pin:button";
const playerInlineKey = "pin:player";
const numericButton = Number.parseInt(properties.button, 10);
const numericPlayer = normalizedPlayer;
properties[buttonInlineKey] = Number.isFinite(numericButton)
? numericButton
: 0;
properties[playerInlineKey] = Number.isFinite(numericPlayer)
? numericPlayer
: 0;
},
onPropertiesChanged: (properties) => {
properties.button = resolveButtonValue(properties.button);
const normalizedPlayer = resolvePlayerIndex(properties.player);
properties.player = String(normalizedPlayer);
const buttonInlineKey = "pin:button";
const playerInlineKey = "pin:player";
const numericButton = Number.parseInt(properties.button, 10);
const numericPlayer = normalizedPlayer;
properties[buttonInlineKey] = Number.isFinite(numericButton)
? numericButton
: 0;
properties[playerInlineKey] = Number.isFinite(numericPlayer)
? numericPlayer
: 0;
},
},
{
evaluateValue: ({ node, resolveValueInput, formatLiteral }) => {
const buttonValue = resolveButtonValue(node.properties.button);
const parsedButton = Number.parseInt(buttonValue, 10);
const buttonIndex = Number.isFinite(parsedButton) ? parsedButton : 0;
const fallbackButton = formatLiteral("number", buttonIndex);
const buttonInput = resolveValueInput("button", fallbackButton);
const fallbackPlayer = formatLiteral(
"number",
resolvePlayerIndex(node.properties.player)
);
const playerInput = resolveValueInput("player", fallbackPlayer);
return `btn(${buttonInput}, ${playerInput})`;
},
}
);

View File

@@ -0,0 +1,17 @@
import { btnNode } from "./btn.js";
import { btnpNode } from "./btnp.js";
import { controllerButtonsNode } from "./controllerButtons.js";
/**
* All input-related node modules backed by PICO-8 helpers.
*/
export const inputNodes = [controllerButtonsNode, btnNode, btnpNode];
/**
* Checklist tracking coverage of documented input helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const inputFunctionChecklist = [
{ function: "BTN", nodeId: "input_btn", implemented: true },
{ function: "BTNP", nodeId: "input_btnp", implemented: true },
];

View File

View File

@@ -0,0 +1,169 @@
import { createNodeModule } from "../nodeTypes.js";
const LOCAL_TYPES = [
{ value: "number", label: "Number" },
{ value: "string", label: "String" },
{ value: "boolean", label: "Boolean" },
{ value: "table", label: "Table" },
];
const DEFAULT_LOCAL_TYPE = LOCAL_TYPES[0]?.value ?? "number";
const sanitizeType = (raw) => {
const candidate = typeof raw === "string" ? raw.toLowerCase() : DEFAULT_LOCAL_TYPE;
return LOCAL_TYPES.some((entry) => entry.value === candidate)
? candidate
: DEFAULT_LOCAL_TYPE;
};
const ensureName = (raw, fallback = "localVar") => {
const value = typeof raw === "string" ? raw.trim() : "";
return value.length ? value : fallback;
};
const resolveValuePropertyKey = (type) => {
switch (type) {
case "string":
return "valueString";
case "boolean":
return "valueBoolean";
case "table":
return "valueTable";
case "number":
default:
return "valueNumber";
}
};
const defaultValueForType = (type) => {
switch (type) {
case "string":
return "";
case "boolean":
return false;
case "table":
return "{}";
case "number":
default:
return 0;
}
};
const initializeLocalProperties = (properties, { includeValue }) => {
const type = sanitizeType(properties.variableType);
properties.variableType = type;
properties.name = ensureName(properties.name);
if (typeof properties.valueNumber !== "number") {
properties.valueNumber = Number.isFinite(Number(properties.valueNumber))
? Number(properties.valueNumber)
: 0;
}
if (typeof properties.valueString !== "string") {
properties.valueString = String(properties.valueString ?? "");
}
if (typeof properties.valueBoolean !== "boolean") {
properties.valueBoolean = Boolean(properties.valueBoolean);
}
if (typeof properties.valueTable !== "string") {
properties.valueTable = String(properties.valueTable ?? "{}");
}
if (includeValue) {
const key = resolveValuePropertyKey(type);
const value = properties[key] ?? defaultValueForType(type);
properties[key] = value;
}
};
const normalizeLocalProperties = (properties, { includeValue }) => {
const type = sanitizeType(properties.variableType);
properties.variableType = type;
properties.name = ensureName(properties.name);
if (includeValue) {
const key = resolveValuePropertyKey(type);
const value = properties[key] ?? defaultValueForType(type);
properties[key] = value;
}
};
export const setLocalVariableNode = createNodeModule(
{
id: "set_local_var",
title: "Set Local",
category: "Logic",
description: "Declare or assign a local variable with an inline literal.",
searchTags: ["local", "set", "assign", "variable"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "value",
name: "Value",
direction: "input",
kind: "any",
description: "Optional override for the inline literal",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
initializeProperties: (properties) => {
initializeLocalProperties(properties, { includeValue: true });
},
onPropertiesChanged: (properties) => {
normalizeLocalProperties(properties, { includeValue: true });
},
},
{
emitExec: ({
node,
indent,
indentLevel,
resolveValueInput,
sanitizeIdentifier,
emitNextExec,
formatLiteral,
}) => {
const type = sanitizeType(node.properties.variableType);
const name = sanitizeIdentifier(ensureName(node.properties.name));
const valueKey = resolveValuePropertyKey(type);
const inlineValue = node.properties[valueKey] ?? defaultValueForType(type);
const fallbackLiteral =
type === "table"
? String(inlineValue ?? "{}") || "{}"
: formatLiteral(type, inlineValue);
const valueExpression = resolveValueInput("value", fallbackLiteral);
const statement = `${indent(indentLevel)}local ${name} = ${valueExpression}`;
return [statement, ...emitNextExec("exec_out")];
},
}
);
export const getLocalVariableNode = createNodeModule(
{
id: "get_local_var",
title: "Get Local",
category: "Logic",
description: "Access a local variable declared earlier in the flow.",
searchTags: ["local", "get", "variable", "read"],
inputs: [],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "any" },
],
properties: [],
initializeProperties: (properties) => {
initializeLocalProperties(properties, { includeValue: false });
},
onPropertiesChanged: (properties) => {
normalizeLocalProperties(properties, { includeValue: false });
},
},
{
evaluateValue: ({ node, sanitizeIdentifier }) => {
return sanitizeIdentifier(ensureName(node.properties.name));
},
}
);
export const localVariableNodes = [setLocalVariableNode, getLocalVariableNode];

View File

@@ -0,0 +1,38 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Creates a coroutine from a function using coroutine.create().
*/
export const luaCoroutineCreateNode = createNodeModule(
{
id: "lua_coroutine_create",
title: "Coroutine Create",
category: "Lua",
description: "Wrap a function in a coroutine via coroutine.create().",
searchTags: ["coroutine", "create", "lua"],
inputs: [
{
id: "fn",
name: "Function",
direction: "input",
kind: "any",
description: "Function to run inside the coroutine",
},
],
outputs: [
{
id: "coroutine",
name: "Coroutine",
direction: "output",
kind: "any",
},
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const fn = resolveValueInput("fn", "function() end");
return `coroutine.create(${fn})`;
},
}
);

View File

@@ -0,0 +1,91 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Resumes a coroutine and exposes the success flag and first result.
*/
export const luaCoroutineResumeNode = createNodeModule(
{
id: "lua_coroutine_resume",
title: "Coroutine Resume",
category: "Lua",
description:
"Resume a coroutine and capture the success flag alongside the first returned value.",
searchTags: ["coroutine", "resume", "lua"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "coroutine",
name: "Coroutine",
direction: "input",
kind: "any",
},
{
id: "arg1",
name: "Arg 1",
direction: "input",
kind: "any",
description: "Optional first value passed to coroutine",
},
{
id: "arg2",
name: "Arg 2",
direction: "input",
kind: "any",
description: "Optional second value passed to coroutine",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
{
id: "success",
name: "Success",
direction: "output",
kind: "boolean",
},
{
id: "result",
name: "Result",
direction: "output",
kind: "any",
},
],
properties: [],
},
{
emitExec: ({
node,
indent,
indentLevel,
resolveValueInput,
sanitizeIdentifier,
emitNextExec,
}) => {
const OMIT = "__pg_omit__";
const target = resolveValueInput("coroutine", "nil");
const arg1 = resolveValueInput("arg1", OMIT);
const arg2 = resolveValueInput("arg2", OMIT);
const args = [target];
if (arg1 !== OMIT) {
args.push(arg1);
}
if (arg2 !== OMIT) {
args.push(arg2);
}
const successName = sanitizeIdentifier(`__pg_${node.id}_co_success`);
const resultName = sanitizeIdentifier(`__pg_${node.id}_co_result`);
const line = `${indent(indentLevel)}local ${successName}, ${resultName} = coroutine.resume(${args.join(", ")})`;
return [line, ...emitNextExec("exec_out")];
},
evaluateValue: ({ node, pinId, sanitizeIdentifier }) => {
const successName = sanitizeIdentifier(`__pg_${node.id}_co_success`);
const resultName = sanitizeIdentifier(`__pg_${node.id}_co_result`);
if (pinId === "success") {
return successName;
}
if (pinId === "result") {
return resultName;
}
return "nil";
},
}
);

View File

@@ -0,0 +1,27 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Returns the currently running coroutine via coroutine.running().
*/
export const luaCoroutineRunningNode = createNodeModule(
{
id: "lua_coroutine_running",
title: "Coroutine Running",
category: "Lua",
description: "Access the currently running coroutine reference.",
searchTags: ["coroutine", "running", "lua"],
inputs: [],
outputs: [
{
id: "coroutine",
name: "Coroutine",
direction: "output",
kind: "any",
},
],
properties: [],
},
{
evaluateValue: () => "coroutine.running()",
}
);

View File

@@ -0,0 +1,32 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reports a coroutine's status via coroutine.status().
*/
export const luaCoroutineStatusNode = createNodeModule(
{
id: "lua_coroutine_status",
title: "Coroutine Status",
category: "Lua",
description: "Retrieve a coroutine's status string using coroutine.status().",
searchTags: ["coroutine", "status", "lua"],
inputs: [
{
id: "coroutine",
name: "Coroutine",
direction: "input",
kind: "any",
},
],
outputs: [
{ id: "status", name: "Status", direction: "output", kind: "string" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const target = resolveValueInput("coroutine", "nil");
return `coroutine.status(${target})`;
},
}
);

View File

@@ -0,0 +1,53 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Yields from within a coroutine using coroutine.yield().
*/
export const luaCoroutineYieldNode = createNodeModule(
{
id: "lua_coroutine_yield",
title: "Coroutine Yield",
category: "Lua",
description: "Yield execution from within a coroutine and optionally return values.",
searchTags: ["coroutine", "yield", "lua"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "value1",
name: "Value 1",
direction: "input",
kind: "any",
description: "Optional first value yielded to the caller",
},
{
id: "value2",
name: "Value 2",
direction: "input",
kind: "any",
description: "Optional second value yielded to the caller",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const value1 = resolveValueInput("value1", OMIT);
const value2 = resolveValueInput("value2", OMIT);
const args = [];
if (value1 !== OMIT) {
args.push(value1);
}
if (value2 !== OMIT) {
args.push(value2);
}
const call = args.length
? `${indent(indentLevel)}coroutine.yield(${args.join(", ")})`
: `${indent(indentLevel)}coroutine.yield()`;
return [call, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,27 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Retrieves the metatable associated with a table via getmetatable().
*/
export const luaGetMetatableNode = createNodeModule(
{
id: "lua_getmetatable",
title: "Get Metatable",
category: "Lua",
description: "Fetch a table's metatable using getmetatable().",
searchTags: ["getmetatable", "metatable", "lua"],
inputs: [
{ id: "table", name: "Table", direction: "input", kind: "table" },
],
outputs: [
{ id: "metatable", name: "Metatable", direction: "output", kind: "table" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const tableValue = resolveValueInput("table", "{}");
return `getmetatable(${tableValue})`;
},
}
);

View File

@@ -0,0 +1,63 @@
import { luaSetMetatableNode } from "./setMetatable.js";
import { luaGetMetatableNode } from "./getMetatable.js";
import { luaRawGetNode } from "./rawGet.js";
import { luaRawSetNode } from "./rawSet.js";
import { luaRawEqualNode } from "./rawEqual.js";
import { luaCoroutineCreateNode } from "./coroutineCreate.js";
import { luaCoroutineResumeNode } from "./coroutineResume.js";
import { luaCoroutineYieldNode } from "./coroutineYield.js";
import { luaCoroutineStatusNode } from "./coroutineStatus.js";
import { luaCoroutineRunningNode } from "./coroutineRunning.js";
/**
* Node modules covering Lua metatable and coroutine helpers.
*/
export const luaNodes = [
luaSetMetatableNode,
luaGetMetatableNode,
luaRawGetNode,
luaRawSetNode,
luaRawEqualNode,
luaCoroutineCreateNode,
luaCoroutineResumeNode,
luaCoroutineYieldNode,
luaCoroutineStatusNode,
luaCoroutineRunningNode,
];
/**
* Checklist tracking coverage of Lua metatable and coroutine helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const luaFunctionChecklist = [
{ function: "SETMETATABLE", nodeId: "lua_setmetatable", implemented: true },
{ function: "GETMETATABLE", nodeId: "lua_getmetatable", implemented: true },
{ function: "RAWGET", nodeId: "lua_rawget", implemented: true },
{ function: "RAWSET", nodeId: "lua_rawset", implemented: true },
{ function: "RAWEQUAL", nodeId: "lua_rawequal", implemented: true },
{
function: "COROUTINE.CREATE",
nodeId: "lua_coroutine_create",
implemented: true,
},
{
function: "COROUTINE.RESUME",
nodeId: "lua_coroutine_resume",
implemented: true,
},
{
function: "COROUTINE.YIELD",
nodeId: "lua_coroutine_yield",
implemented: true,
},
{
function: "COROUTINE.STATUS",
nodeId: "lua_coroutine_status",
implemented: true,
},
{
function: "COROUTINE.RUNNING",
nodeId: "lua_coroutine_running",
implemented: true,
},
];

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Compares two values using rawequal().
*/
export const luaRawEqualNode = createNodeModule(
{
id: "lua_rawequal",
title: "Raw Equal",
category: "Lua",
description: "Compare values without metamethods using rawequal().",
searchTags: ["rawequal", "compare", "lua"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "any" },
{ id: "b", name: "B", direction: "input", kind: "any" },
],
outputs: [
{ id: "equal", name: "Equal", direction: "output", kind: "boolean" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "nil");
const b = resolveValueInput("b", "nil");
return `rawequal(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Retrieves a raw table entry using rawget().
*/
export const luaRawGetNode = createNodeModule(
{
id: "lua_rawget",
title: "Raw Get",
category: "Lua",
description: "Access a table entry without metamethods using rawget().",
searchTags: ["rawget", "table", "lua"],
inputs: [
{ id: "table", name: "Table", direction: "input", kind: "table" },
{ id: "key", name: "Key", direction: "input", kind: "any" },
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "any" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const tableValue = resolveValueInput("table", "{}");
const key = resolveValueInput("key", "nil");
return `rawget(${tableValue}, ${key})`;
},
}
);

View File

@@ -0,0 +1,33 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Writes a raw table entry using rawset().
*/
export const luaRawSetNode = createNodeModule(
{
id: "lua_rawset",
title: "Raw Set",
category: "Lua",
description: "Assign a table entry without metamethods using rawset().",
searchTags: ["rawset", "table", "lua"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{ id: "table", name: "Table", direction: "input", kind: "table" },
{ id: "key", name: "Key", direction: "input", kind: "any" },
{ id: "value", name: "Value", direction: "input", kind: "any" },
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const tableValue = resolveValueInput("table", "{}");
const key = resolveValueInput("key", "nil");
const value = resolveValueInput("value", "nil");
const line = `${indent(indentLevel)}rawset(${tableValue}, ${key}, ${value})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,35 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Applies a metatable to a table via setmetatable().
*/
export const luaSetMetatableNode = createNodeModule(
{
id: "lua_setmetatable",
title: "Set Metatable",
category: "Lua",
description: "Assign a metatable to a table using setmetatable().",
searchTags: ["setmetatable", "metatable", "lua"],
inputs: [
{ id: "table", name: "Table", direction: "input", kind: "table" },
{
id: "metatable",
name: "Metatable",
direction: "input",
kind: "table",
defaultValue: "nil",
},
],
outputs: [
{ id: "table_out", name: "Table", direction: "output", kind: "table" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const tableValue = resolveValueInput("table", "{}");
const metatable = resolveValueInput("metatable", "nil");
return `setmetatable(${tableValue}, ${metatable})`;
},
}
);

View File

@@ -0,0 +1,20 @@
import { mapMgetNode } from "./mget.js";
import { mapMsetNode } from "./mset.js";
import { mapDrawNode } from "./map.js";
import { mapTlineNode } from "./tline.js";
/**
* All map-related node modules backed by PICO-8 helpers.
*/
export const mapNodes = [mapMgetNode, mapMsetNode, mapDrawNode, mapTlineNode];
/**
* Checklist tracking coverage of documented map helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const mapFunctionChecklist = [
{ function: "MGET", nodeId: "map_mget", implemented: true },
{ function: "MSET", nodeId: "map_mset", implemented: true },
{ function: "MAP", nodeId: "map_map", implemented: true },
{ function: "TLINE", nodeId: "map_tline", implemented: true },
];

View File

@@ -0,0 +1,108 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws map tiles using PICO-8's map helper.
*/
export const mapDrawNode = createNodeModule(
{
id: "map_map",
title: "Draw Map",
category: "Map",
description:
"Render a section of the map to the screen with optional layers.",
searchTags: ["map", "draw", "tiles", "layer"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "tile_x",
name: "Tile X",
direction: "input",
kind: "number",
description: "Starting tile X coordinate",
},
{
id: "tile_y",
name: "Tile Y",
direction: "input",
kind: "number",
description: "Starting tile Y coordinate",
},
{
id: "screen_x",
name: "Screen X",
direction: "input",
kind: "number",
description: "Screen X position in pixels",
},
{
id: "screen_y",
name: "Screen Y",
direction: "input",
kind: "number",
description: "Screen Y position in pixels",
},
{
id: "tile_w",
name: "Tile Width",
direction: "input",
kind: "number",
description: "Number of tiles across",
},
{
id: "tile_h",
name: "Tile Height",
direction: "input",
kind: "number",
description: "Number of tiles down",
},
{
id: "layers",
name: "Layers",
direction: "input",
kind: "number",
description: "Sprite flag bitmask",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const OMIT = "__pg_omit__";
const tileX = resolveValueInput("tile_x", OMIT);
const tileY = resolveValueInput("tile_y", OMIT);
const screenX = resolveValueInput("screen_x", OMIT);
const screenY = resolveValueInput("screen_y", OMIT);
const tileW = resolveValueInput("tile_w", OMIT);
const tileH = resolveValueInput("tile_h", OMIT);
const layers = resolveValueInput("layers", OMIT);
const values = [
{ value: tileX, placeholder: "0" },
{ value: tileY, placeholder: "0" },
{ value: screenX, placeholder: "0" },
{ value: screenY, placeholder: "0" },
{ value: tileW, placeholder: "nil" },
{ value: tileH, placeholder: "nil" },
{ value: layers, placeholder: "nil" },
];
let lastIndex = values.length - 1;
while (lastIndex >= 0 && values[lastIndex].value === OMIT) {
lastIndex -= 1;
}
const args = [];
for (let index = 0; index <= lastIndex; index += 1) {
const entry = values[index];
args.push(entry.value === OMIT ? entry.placeholder : entry.value);
}
const call = args.length ? `map(${args.join(", ")})` : "map()";
const line = `${indent(indentLevel)}${call}`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,41 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Reads a map tile value using PICO-8's mget helper.
*/
export const mapMgetNode = createNodeModule(
{
id: "map_mget",
title: "Get Map Tile",
category: "Map",
description: "Fetch the numeric map value at a tile coordinate.",
searchTags: ["mget", "map", "tile", "read"],
inputs: [
{
id: "x",
name: "Tile X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Tile Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "value", name: "Value", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
return `mget(${x}, ${y})`;
},
}
);

View File

@@ -0,0 +1,51 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Writes a map tile value using PICO-8's mset helper.
*/
export const mapMsetNode = createNodeModule(
{
id: "map_mset",
title: "Set Map Tile",
category: "Map",
description: "Store a numeric value at a tile coordinate.",
searchTags: ["mset", "map", "tile", "write"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x",
name: "Tile X",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "y",
name: "Tile Y",
direction: "input",
kind: "number",
defaultValue: 0,
},
{
id: "value",
name: "Value",
direction: "input",
kind: "number",
defaultValue: 0,
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [],
},
{
emitExec: ({ indent, indentLevel, resolveValueInput, emitNextExec }) => {
const x = resolveValueInput("x", "0");
const y = resolveValueInput("y", "0");
const value = resolveValueInput("value", "0");
const line = `${indent(indentLevel)}mset(${x}, ${y}, ${value})`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,157 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Draws textured lines or configures precision using PICO-8's tline helper.
*/
export const mapTlineNode = createNodeModule(
{
id: "map_tline",
title: "Draw Textured Line",
category: "Map",
description:
"Render a textured line from map data or adjust tline precision.",
searchTags: ["tline", "map", "texture", "line"],
inputs: [
{ id: "exec_in", name: "Exec", direction: "input", kind: "exec" },
{
id: "x0",
name: "X0",
direction: "input",
kind: "number",
description: "Line start X",
},
{
id: "y0",
name: "Y0",
direction: "input",
kind: "number",
description: "Line start Y",
},
{
id: "x1",
name: "X1",
direction: "input",
kind: "number",
description: "Line end X",
},
{
id: "y1",
name: "Y1",
direction: "input",
kind: "number",
description: "Line end Y",
},
{
id: "mx",
name: "Map X",
direction: "input",
kind: "number",
description: "Map sample X (tiles)",
},
{
id: "my",
name: "Map Y",
direction: "input",
kind: "number",
description: "Map sample Y (tiles)",
},
{
id: "mdx",
name: "MDX",
direction: "input",
kind: "number",
description: "Map X delta",
},
{
id: "mdy",
name: "MDY",
direction: "input",
kind: "number",
description: "Map Y delta",
},
{
id: "layers",
name: "Layers",
direction: "input",
kind: "number",
description: "Sprite flag bitmask",
},
{
id: "precision",
name: "Precision",
direction: "input",
kind: "number",
description: "Precision override when setting mode",
},
],
outputs: [
{ id: "exec_out", name: "Exec", direction: "output", kind: "exec" },
],
properties: [
{
key: "mode",
label: "Mode",
type: "enum",
defaultValue: "draw",
options: [
{ label: "Draw Line", value: "draw" },
{ label: "Set Precision", value: "precision" },
],
},
],
},
{
emitExec: ({
node,
indent,
indentLevel,
resolveValueInput,
emitNextExec,
}) => {
const mode = String(node.properties.mode ?? "draw");
if (mode === "precision") {
const precision = resolveValueInput("precision", "16");
const line = `${indent(indentLevel)}tline(${precision})`;
return [line, ...emitNextExec("exec_out")];
}
const OMIT = "__pg_omit__";
const x0 = resolveValueInput("x0", OMIT);
const y0 = resolveValueInput("y0", OMIT);
const x1 = resolveValueInput("x1", OMIT);
const y1 = resolveValueInput("y1", OMIT);
const mx = resolveValueInput("mx", OMIT);
const my = resolveValueInput("my", OMIT);
const mdx = resolveValueInput("mdx", OMIT);
const mdy = resolveValueInput("mdy", OMIT);
const layers = resolveValueInput("layers", OMIT);
const values = [
{ value: x0, placeholder: "0" },
{ value: y0, placeholder: "0" },
{ value: x1, placeholder: "0" },
{ value: y1, placeholder: "0" },
{ value: mx, placeholder: "0" },
{ value: my, placeholder: "0" },
{ value: mdx, placeholder: "0.125" },
{ value: mdy, placeholder: "0" },
{ value: layers, placeholder: "nil" },
];
let lastIndex = values.length - 1;
while (lastIndex >= 0 && values[lastIndex].value === OMIT) {
lastIndex -= 1;
}
const args = [];
for (let index = 0; index <= lastIndex; index += 1) {
const entry = values[index];
args.push(entry.value === OMIT ? entry.placeholder : entry.value);
}
const call = args.length ? `tline(${args.join(", ")})` : "tline()";
const line = `${indent(indentLevel)}${call}`;
return [line, ...emitNextExec("exec_out")];
},
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Computes the absolute value using abs.
*/
export const mathAbsNode = createNodeModule(
{
id: "math_abs",
title: "Abs",
category: "Math",
description: "Return the absolute value of a number.",
searchTags: ["abs", "absolute", "math"],
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
return `abs(${value})`;
},
}
);

View File

@@ -0,0 +1,30 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Computes an angle from delta components using atan2.
*/
export const mathAtan2Node = createNodeModule(
{
id: "math_atan2",
title: "Atan2",
category: "Math",
description:
"Return the screenspace angle for a delta X and Y pair.",
searchTags: ["atan2", "angle", "math", "trig"],
inputs: [
{ id: "dx", name: "DX", direction: "input", kind: "number" },
{ id: "dy", name: "DY", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const dx = resolveValueInput("dx", "0");
const dy = resolveValueInput("dy", "0");
return `atan2(${dx}, ${dy})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs bitwise and using band.
*/
export const mathBandNode = createNodeModule(
{
id: "math_band",
title: "Bitwise AND",
category: "Math",
description: "Return bits set in both operands using BAND().",
searchTags: ["band", "bitwise", "and", "math"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `band(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs bitwise not using bnot.
*/
export const mathBnotNode = createNodeModule(
{
id: "math_bnot",
title: "Bitwise NOT",
category: "Math",
description: "Invert every bit in a value using BNOT().",
searchTags: ["bnot", "bitwise", "not", "math"],
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
return `bnot(${value})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs bitwise or using bor.
*/
export const mathBorNode = createNodeModule(
{
id: "math_bor",
title: "Bitwise OR",
category: "Math",
description: "Return bits set in either operand using BOR().",
searchTags: ["bor", "bitwise", "or", "math"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `bor(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs bitwise exclusive-or using bxor.
*/
export const mathBxorNode = createNodeModule(
{
id: "math_bxor",
title: "Bitwise XOR",
category: "Math",
description: "Return bits set in exactly one operand using BXOR().",
searchTags: ["bxor", "bitwise", "xor", "math"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `bxor(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Ceils a numeric value using ceil.
*/
export const mathCeilNode = createNodeModule(
{
id: "math_ceil",
title: "Ceil",
category: "Math",
description: "Round a value up to the nearest integer.",
searchTags: ["ceil", "ceiling", "math", "round"],
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
return `ceil(${value})`;
},
}
);

View File

@@ -0,0 +1,26 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Computes the cosine of a value using cos.
*/
export const mathCosNode = createNodeModule(
{
id: "math_cos",
title: "Cos",
category: "Math",
description:
"Return the cosine of an angle where 1.0 represents a full turn.",
searchTags: ["cos", "cosine", "math", "trig"],
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
return `cos(${value})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs integer division using the floor operation.
*/
export const mathDivNode = createNodeModule(
{
id: "math_div",
title: "Integer Divide",
category: "Math",
description: "Return the floored quotient of two numbers (A \\ B).",
searchTags: ["div", "integer", "divide", "math"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "1");
return `(${a}) \\ (${b})`;
},
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Floors a numeric value using flr.
*/
export const mathFlrNode = createNodeModule(
{
id: "math_flr",
title: "Floor",
category: "Math",
description: "Round a value down to the nearest integer.",
searchTags: ["flr", "floor", "math", "round"],
inputs: [{ id: "value", name: "Value", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
return `flr(${value})`;
},
}
);

View File

@@ -0,0 +1,79 @@
import { mathMaxNode } from "./max.js";
import { mathMinNode } from "./min.js";
import { mathMidNode } from "./mid.js";
import { mathFlrNode } from "./flr.js";
import { mathCeilNode } from "./ceil.js";
import { mathCosNode } from "./cos.js";
import { mathSinNode } from "./sin.js";
import { mathAtan2Node } from "./atan2.js";
import { mathSqrtNode } from "./sqrt.js";
import { mathAbsNode } from "./abs.js";
import { mathRndNode } from "./rnd.js";
import { mathSrandNode } from "./srand.js";
import { mathBandNode } from "./band.js";
import { mathBorNode } from "./bor.js";
import { mathBxorNode } from "./bxor.js";
import { mathBnotNode } from "./bnot.js";
import { mathShlNode } from "./shl.js";
import { mathShrNode } from "./shr.js";
import { mathLshrNode } from "./lshr.js";
import { mathRotlNode } from "./rotl.js";
import { mathRotrNode } from "./rotr.js";
import { mathDivNode } from "./div.js";
/**
* All math-related node modules backed by PICO-8 helpers.
*/
export const mathNodes = [
mathMaxNode,
mathMinNode,
mathMidNode,
mathFlrNode,
mathCeilNode,
mathCosNode,
mathSinNode,
mathAtan2Node,
mathSqrtNode,
mathAbsNode,
mathRndNode,
mathSrandNode,
mathBandNode,
mathBorNode,
mathBxorNode,
mathBnotNode,
mathShlNode,
mathShrNode,
mathLshrNode,
mathRotlNode,
mathRotrNode,
mathDivNode,
];
/**
* Checklist tracking coverage of documented math helpers.
* @type {Array<{ function: string, nodeId: string, implemented: boolean }>}
*/
export const mathFunctionChecklist = [
{ function: "MAX", nodeId: "math_max", implemented: true },
{ function: "MIN", nodeId: "math_min", implemented: true },
{ function: "MID", nodeId: "math_mid", implemented: true },
{ function: "FLR", nodeId: "math_flr", implemented: true },
{ function: "CEIL", nodeId: "math_ceil", implemented: true },
{ function: "COS", nodeId: "math_cos", implemented: true },
{ function: "SIN", nodeId: "math_sin", implemented: true },
{ function: "ATAN2", nodeId: "math_atan2", implemented: true },
{ function: "SQRT", nodeId: "math_sqrt", implemented: true },
{ function: "ABS", nodeId: "math_abs", implemented: true },
{ function: "RND", nodeId: "math_rnd", implemented: true },
{ function: "SRAND", nodeId: "math_srand", implemented: true },
{ function: "BAND", nodeId: "math_band", implemented: true },
{ function: "BOR", nodeId: "math_bor", implemented: true },
{ function: "BXOR", nodeId: "math_bxor", implemented: true },
{ function: "BNOT", nodeId: "math_bnot", implemented: true },
{ function: "SHL", nodeId: "math_shl", implemented: true },
{ function: "SHR", nodeId: "math_shr", implemented: true },
{ function: "LSHR", nodeId: "math_lshr", implemented: true },
{ function: "ROTL", nodeId: "math_rotl", implemented: true },
{ function: "ROTR", nodeId: "math_rotr", implemented: true },
{ function: "\\", nodeId: "math_div", implemented: true },
];

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs a logical right bit shift using lshr.
*/
export const mathLshrNode = createNodeModule(
{
id: "math_lshr",
title: "Logical Shift Right",
category: "Math",
description: "Shift bits right with zeros entering from the left.",
searchTags: ["lshr", "shift", "bitwise", "math"],
inputs: [
{ id: "value", name: "Value", direction: "input", kind: "number" },
{ id: "amount", name: "Amount", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
const amount = resolveValueInput("amount", "0");
return `lshr(${value}, ${amount})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Returns the maximum of two values using max.
*/
export const mathMaxNode = createNodeModule(
{
id: "math_max",
title: "Max",
category: "Math",
description: "Return the greater of two numeric values.",
searchTags: ["max", "maximum", "math", "compare"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "value", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `max(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,31 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Returns the median of three values using mid.
*/
export const mathMidNode = createNodeModule(
{
id: "math_mid",
title: "Mid",
category: "Math",
description: "Return the middle value from three numeric inputs.",
searchTags: ["mid", "median", "math", "compare"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
{ id: "c", name: "C", direction: "input", kind: "number" },
],
outputs: [
{ id: "value", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
const c = resolveValueInput("c", "0");
return `mid(${a}, ${b}, ${c})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Returns the minimum of two values using min.
*/
export const mathMinNode = createNodeModule(
{
id: "math_min",
title: "Min",
category: "Math",
description: "Return the lesser of two numeric values.",
searchTags: ["min", "minimum", "math", "compare"],
inputs: [
{ id: "a", name: "A", direction: "input", kind: "number" },
{ id: "b", name: "B", direction: "input", kind: "number" },
],
outputs: [
{ id: "value", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const a = resolveValueInput("a", "0");
const b = resolveValueInput("b", "0");
return `min(${a}, ${b})`;
},
}
);

View File

@@ -0,0 +1,25 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Generates random numbers using rnd.
*/
export const mathRndNode = createNodeModule(
{
id: "math_rnd",
title: "Random",
category: "Math",
description: "Return a random number in the range [0, x).",
searchTags: ["rnd", "random", "math"],
inputs: [{ id: "value", name: "Range", direction: "input", kind: "number" }],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "1");
return `rnd(${value})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs a left bit rotation using rotl.
*/
export const mathRotlNode = createNodeModule(
{
id: "math_rotl",
title: "Rotate Left",
category: "Math",
description: "Rotate bits left by a given amount.",
searchTags: ["rotl", "rotate", "bitwise", "math"],
inputs: [
{ id: "value", name: "Value", direction: "input", kind: "number" },
{ id: "amount", name: "Amount", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
const amount = resolveValueInput("amount", "0");
return `rotl(${value}, ${amount})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs a right bit rotation using rotr.
*/
export const mathRotrNode = createNodeModule(
{
id: "math_rotr",
title: "Rotate Right",
category: "Math",
description: "Rotate bits right by a given amount.",
searchTags: ["rotr", "rotate", "bitwise", "math"],
inputs: [
{ id: "value", name: "Value", direction: "input", kind: "number" },
{ id: "amount", name: "Amount", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
const amount = resolveValueInput("amount", "0");
return `rotr(${value}, ${amount})`;
},
}
);

View File

@@ -0,0 +1,29 @@
import { createNodeModule } from "../../nodeTypes.js";
/**
* Performs a left bit shift using shl.
*/
export const mathShlNode = createNodeModule(
{
id: "math_shl",
title: "Shift Left",
category: "Math",
description: "Shift bits left with zeros entering from the right.",
searchTags: ["shl", "shift", "bitwise", "math"],
inputs: [
{ id: "value", name: "Value", direction: "input", kind: "number" },
{ id: "amount", name: "Amount", direction: "input", kind: "number" },
],
outputs: [
{ id: "result", name: "Result", direction: "output", kind: "number" },
],
properties: [],
},
{
evaluateValue: ({ resolveValueInput }) => {
const value = resolveValueInput("value", "0");
const amount = resolveValueInput("amount", "0");
return `shl(${value}, ${amount})`;
},
}
);

Some files were not shown because too many files have changed in this diff Show More