mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 10:46:06 +10:00
Add initial Pico-8 cartridges for testing
- Created test_cart.p8 with basic drawing functionality and a print statement. - Created test_unix.p8 with a simple initialization function.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { formatLiteral, defaultLiteralForKind } from './lua/LuaLiteralTypes.js';
|
||||
import { normalizeVariableType, getTypeConverter } from '../types/TypeRegistry.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('./NodeGraph.js').NodeGraph} NodeGraph
|
||||
*/
|
||||
@@ -278,7 +281,7 @@ export class LuaGenerator {
|
||||
findExecTargets: (pinId) => this.#findExecTargets(node.id, pinId),
|
||||
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
||||
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
||||
formatLiteral: (kind, value) => this.#formatLiteral(kind, value),
|
||||
formatLiteral,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -338,7 +341,7 @@ export class LuaGenerator {
|
||||
this.#resolveValueInput(node, inputPinId, fallback),
|
||||
sanitizeIdentifier: (value) => this.#sanitizeIdentifier(String(value)),
|
||||
sanitizeOperator: (value) => this.#sanitizeOperator(String(value)),
|
||||
formatLiteral: (kind, value) => this.#formatLiteral(kind, value),
|
||||
formatLiteral,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -516,7 +519,7 @@ export class LuaGenerator {
|
||||
const argEntries = signature.map((entry) => {
|
||||
const fallback = entry.optional
|
||||
? "nil"
|
||||
: this.#defaultLiteralForKind(entry.kind);
|
||||
: defaultLiteralForKind(entry.kind);
|
||||
const value = this.#resolveValueInput(
|
||||
node,
|
||||
entry.inputPinId,
|
||||
@@ -602,6 +605,49 @@ export class LuaGenerator {
|
||||
.map((connection) => connection.to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Lua type conversion when source and target types differ.
|
||||
*
|
||||
* @param {string} expression Source expression to convert.
|
||||
* @param {string} fromType Source type identifier.
|
||||
* @param {string} toType Target type identifier.
|
||||
* @returns {string}
|
||||
*/
|
||||
#applyTypeConversion(expression, fromType, toType) {
|
||||
if (fromType === toType || toType === "any" || fromType === "any") {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const converter = getTypeConverter(fromType, toType);
|
||||
if (!converter) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
// Generate Lua code for type conversions
|
||||
if ((fromType === "number" || fromType === "color") && toType === "boolean") {
|
||||
return `(${expression}) ~= 0`;
|
||||
}
|
||||
|
||||
if (fromType === "boolean" && toType === "number") {
|
||||
return `((${expression}) and 1 or 0)`;
|
||||
}
|
||||
|
||||
if (fromType === "boolean" && toType === "color") {
|
||||
return `((${expression}) and 7 or 0)`;
|
||||
}
|
||||
|
||||
if (fromType === "boolean" && toType === "string") {
|
||||
return `((${expression}) and "true" or "false")`;
|
||||
}
|
||||
|
||||
if (fromType === "number" && toType === "color") {
|
||||
return `flr(${expression}) % 16`;
|
||||
}
|
||||
|
||||
// No conversion needed for color to number (colors are numbers in PICO-8)
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the expression bound to an input pin.
|
||||
*
|
||||
@@ -613,7 +659,17 @@ export class LuaGenerator {
|
||||
#resolveValueInput(node, pinId, fallback) {
|
||||
const connection = this.#getConnectionTo(node.id, pinId);
|
||||
if (connection) {
|
||||
return this.#evaluateValue(connection.from.nodeId, connection.from.pinId);
|
||||
const sourceExpression = this.#evaluateValue(connection.from.nodeId, connection.from.pinId);
|
||||
const targetPin = this.#findPin(node, pinId);
|
||||
const targetType = targetPin?.kind ?? "any";
|
||||
|
||||
// Get the actual source type, handling special cases like get_var
|
||||
const sourceNode = this.nodes.get(connection.from.nodeId);
|
||||
const sourceType = sourceNode
|
||||
? this.#getOutputPinType(sourceNode, connection.from.pinId)
|
||||
: connection.kind ?? "any";
|
||||
|
||||
return this.#applyTypeConversion(sourceExpression, sourceType, targetType);
|
||||
}
|
||||
|
||||
const pin = this.#findPin(node, pinId);
|
||||
@@ -621,12 +677,12 @@ export class LuaGenerator {
|
||||
if (Object.prototype.hasOwnProperty.call(node.properties, inlineKey)) {
|
||||
const inlineValue = node.properties[inlineKey];
|
||||
if (inlineValue !== undefined && inlineValue !== null) {
|
||||
return this.#formatLiteral(pin?.kind ?? "any", inlineValue);
|
||||
return formatLiteral(pin?.kind ?? "any", inlineValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (pin && pin.defaultValue !== undefined && pin.defaultValue !== null) {
|
||||
return this.#formatLiteral(pin.kind, pin.defaultValue);
|
||||
return formatLiteral(pin.kind, pin.defaultValue);
|
||||
}
|
||||
|
||||
return fallback;
|
||||
@@ -691,24 +747,6 @@ export class LuaGenerator {
|
||||
|
||||
if (expression === undefined) {
|
||||
switch (node.type) {
|
||||
case "number_literal":
|
||||
expression = this.#formatLiteral(
|
||||
"number",
|
||||
node.properties.value ?? 0
|
||||
);
|
||||
break;
|
||||
case "string_literal":
|
||||
expression = this.#formatLiteral(
|
||||
"string",
|
||||
node.properties.value ?? ""
|
||||
);
|
||||
break;
|
||||
case "boolean_literal":
|
||||
expression = this.#formatLiteral(
|
||||
"boolean",
|
||||
node.properties.value ?? "true"
|
||||
);
|
||||
break;
|
||||
case "get_var": {
|
||||
const name = this.#resolveWorkspaceVariableName(node);
|
||||
expression = name;
|
||||
@@ -721,18 +759,32 @@ export class LuaGenerator {
|
||||
expression = parameterName ?? "nil";
|
||||
break;
|
||||
}
|
||||
case "add_number": {
|
||||
case "add_number":
|
||||
case "math_add": {
|
||||
const a = this.#resolveValueInput(node, "a", "0");
|
||||
const b = this.#resolveValueInput(node, "b", "0");
|
||||
expression = `(${a}) + (${b})`;
|
||||
break;
|
||||
}
|
||||
case "multiply_number": {
|
||||
case "math_subtract": {
|
||||
const a = this.#resolveValueInput(node, "a", "0");
|
||||
const b = this.#resolveValueInput(node, "b", "0");
|
||||
expression = `(${a}) - (${b})`;
|
||||
break;
|
||||
}
|
||||
case "multiply_number":
|
||||
case "math_multiply": {
|
||||
const a = this.#resolveValueInput(node, "a", "1");
|
||||
const b = this.#resolveValueInput(node, "b", "1");
|
||||
expression = `(${a}) * (${b})`;
|
||||
break;
|
||||
}
|
||||
case "math_divide": {
|
||||
const a = this.#resolveValueInput(node, "a", "0");
|
||||
const b = this.#resolveValueInput(node, "b", "1");
|
||||
expression = `(${a}) / (${b})`;
|
||||
break;
|
||||
}
|
||||
case "compare": {
|
||||
const a = this.#resolveValueInput(node, "a", "0");
|
||||
const b = this.#resolveValueInput(node, "b", "0");
|
||||
@@ -788,60 +840,6 @@ export class LuaGenerator {
|
||||
return "==";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a literal based on pin kind.
|
||||
*
|
||||
* @param {string} kind Pin kind.
|
||||
* @param {unknown} value Raw value.
|
||||
* @returns {string}
|
||||
*/
|
||||
#formatLiteral(kind, value) {
|
||||
switch (kind) {
|
||||
case "number": {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? String(numeric) : "0";
|
||||
}
|
||||
case "boolean": {
|
||||
if (typeof value === "string") {
|
||||
return value === "false" ? "false" : "true";
|
||||
}
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
case "table": {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : "{}";
|
||||
}
|
||||
return "{}";
|
||||
}
|
||||
case "string":
|
||||
return JSON.stringify(String(value ?? ""));
|
||||
default:
|
||||
return JSON.stringify(String(value ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a default literal for the supplied pin kind.
|
||||
*
|
||||
* @param {string} kind Pin kind reference.
|
||||
* @returns {string}
|
||||
*/
|
||||
#defaultLiteralForKind(kind) {
|
||||
switch (kind) {
|
||||
case "number":
|
||||
return "0";
|
||||
case "boolean":
|
||||
return "false";
|
||||
case "string":
|
||||
return '""';
|
||||
case "table":
|
||||
return "{}";
|
||||
default:
|
||||
return "nil";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the display name for a custom event node.
|
||||
*
|
||||
@@ -1012,14 +1010,11 @@ export class LuaGenerator {
|
||||
const value = variable?.defaultValue;
|
||||
|
||||
switch (type) {
|
||||
case "number": {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? String(numeric) : "0";
|
||||
}
|
||||
case "number":
|
||||
case "boolean":
|
||||
return value ? "true" : "false";
|
||||
case "string":
|
||||
return JSON.stringify(String(value ?? ""));
|
||||
case "color":
|
||||
return formatLiteral(type, value);
|
||||
case "table": {
|
||||
if (Array.isArray(value)) {
|
||||
const fragments = value
|
||||
@@ -1137,6 +1132,38 @@ export class LuaGenerator {
|
||||
return this.#sanitizeIdentifier(fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of a workspace variable by its ID.
|
||||
*
|
||||
* @param {string} variableId Variable identifier.
|
||||
* @returns {string}
|
||||
*/
|
||||
#getVariableType(variableId) {
|
||||
const variable = this.variables.find((v) => v.id === variableId);
|
||||
return variable?.type ?? "any";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output type for a node's output pin.
|
||||
*
|
||||
* @param {BlueprintNode} node Source node.
|
||||
* @param {string} pinId Output pin identifier.
|
||||
* @returns {string}
|
||||
*/
|
||||
#getOutputPinType(node, pinId) {
|
||||
// For get_var nodes, return the actual variable type
|
||||
if (node.type === "get_var") {
|
||||
const variableId = node.properties?.variableId;
|
||||
if (variableId) {
|
||||
return this.#getVariableType(variableId);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, use the pin's declared kind
|
||||
const pin = this.#findPin(node, pinId);
|
||||
return pin?.kind ?? "any";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a variable type descriptor.
|
||||
*
|
||||
@@ -1144,19 +1171,7 @@ export class LuaGenerator {
|
||||
* @returns {string}
|
||||
*/
|
||||
#normalizeVariableType(value) {
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case "number":
|
||||
case "string":
|
||||
case "boolean":
|
||||
case "table":
|
||||
return normalized;
|
||||
default:
|
||||
return "any";
|
||||
}
|
||||
}
|
||||
return "any";
|
||||
return normalizeVariableType(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BlueprintNode } from "./BlueprintNode.js";
|
||||
import { Connection } from "./Connection.js";
|
||||
import { areTypesCompatible } from "../types/TypeRegistry.js";
|
||||
|
||||
/** @typedef {import('./BlueprintNode.js').BlueprintNodeInit} BlueprintNodeInit */
|
||||
/** @typedef {import('./BlueprintNode.js').PinDescriptor} PinDescriptor */
|
||||
@@ -321,11 +322,7 @@ export class NodeGraph extends EventTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
toPin.kind !== "any" &&
|
||||
fromPin.kind !== "any" &&
|
||||
fromPin.kind !== toPin.kind
|
||||
) {
|
||||
if (!areTypesCompatible(fromPin.kind, toPin.kind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
268
scripts/core/SpriteSheet.js
Normal file
268
scripts/core/SpriteSheet.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Manages PICO-8 sprite sheet data with 256 8x8 sprites.
|
||||
* Total sheet size: 128x128 pixels, 16-color palette.
|
||||
*/
|
||||
export class SpriteSheet {
|
||||
/**
|
||||
* @param {Object} [options={}] Configuration options.
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
/** @type {Uint8Array} Pixel data (128x128, each byte is a palette index 0-15) */
|
||||
this.pixels = new Uint8Array(128 * 128);
|
||||
|
||||
/** @type {Uint8Array} Sprite flags (256 sprites, 8 flags each) */
|
||||
this.flags = new Uint8Array(256);
|
||||
|
||||
/** @type {Array<string>} PICO-8 default 16-color palette */
|
||||
this.palette = [
|
||||
"#000000", "#1D2B53", "#7E2553", "#008751",
|
||||
"#AB5236", "#5F574F", "#C2C3C7", "#FFF1E8",
|
||||
"#FF004D", "#FFA300", "#FFEC27", "#00E436",
|
||||
"#29ADFF", "#83769C", "#FF77A8", "#FFCCAA"
|
||||
];
|
||||
|
||||
this.#initialize(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes sprite sheet data.
|
||||
*
|
||||
* @param {Object} options Configuration options.
|
||||
*/
|
||||
#initialize(options) {
|
||||
if (options.pixels instanceof Uint8Array && options.pixels.length === 128 * 128) {
|
||||
this.pixels.set(options.pixels);
|
||||
}
|
||||
|
||||
if (options.flags instanceof Uint8Array && options.flags.length === 256) {
|
||||
this.flags.set(options.flags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pixel color at coordinates.
|
||||
*
|
||||
* @param {number} x X coordinate (0-127).
|
||||
* @param {number} y Y coordinate (0-127).
|
||||
* @returns {number} Palette index (0-15).
|
||||
*/
|
||||
getPixel(x, y) {
|
||||
if (x < 0 || x >= 128 || y < 0 || y >= 128) return 0;
|
||||
return this.pixels[y * 128 + x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets pixel color at coordinates.
|
||||
*
|
||||
* @param {number} x X coordinate (0-127).
|
||||
* @param {number} y Y coordinate (0-127).
|
||||
* @param {number} color Palette index (0-15).
|
||||
*/
|
||||
setPixel(x, y, color) {
|
||||
if (x < 0 || x >= 128 || y < 0 || y >= 128) return;
|
||||
this.pixels[y * 128 + x] = Math.max(0, Math.min(15, color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all pixels for a specific sprite.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @returns {Uint8Array} 8x8 pixel array.
|
||||
*/
|
||||
getSprite(spriteIndex) {
|
||||
const spriteData = new Uint8Array(8 * 8);
|
||||
const sx = (spriteIndex % 16) * 8;
|
||||
const sy = Math.floor(spriteIndex / 16) * 8;
|
||||
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
spriteData[y * 8 + x] = this.getPixel(sx + x, sy + y);
|
||||
}
|
||||
}
|
||||
|
||||
return spriteData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all pixels for a specific sprite.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @param {Uint8Array} data 8x8 pixel array.
|
||||
*/
|
||||
setSprite(spriteIndex, data) {
|
||||
if (data.length !== 64) return;
|
||||
|
||||
const sx = (spriteIndex % 16) * 8;
|
||||
const sy = Math.floor(spriteIndex / 16) * 8;
|
||||
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
this.setPixel(sx + x, sy + y, data[y * 8 + x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets flags for a sprite.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @returns {number} 8-bit flag value.
|
||||
*/
|
||||
getFlags(spriteIndex) {
|
||||
if (spriteIndex < 0 || spriteIndex >= 256) return 0;
|
||||
return this.flags[spriteIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets flags for a sprite.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @param {number} flagValue 8-bit flag value.
|
||||
*/
|
||||
setFlags(spriteIndex, flagValue) {
|
||||
if (spriteIndex < 0 || spriteIndex >= 256) return;
|
||||
this.flags[spriteIndex] = flagValue & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific flag is set.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @param {number} flagBit Flag bit (0-7).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasFlag(spriteIndex, flagBit) {
|
||||
if (flagBit < 0 || flagBit > 7) return false;
|
||||
return (this.getFlags(spriteIndex) & (1 << flagBit)) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific flag.
|
||||
*
|
||||
* @param {number} spriteIndex Sprite number (0-255).
|
||||
* @param {number} flagBit Flag bit (0-7).
|
||||
* @param {boolean} value Flag state.
|
||||
*/
|
||||
setFlag(spriteIndex, flagBit, value) {
|
||||
if (flagBit < 0 || flagBit > 7) return;
|
||||
|
||||
const currentFlags = this.getFlags(spriteIndex);
|
||||
const mask = 1 << flagBit;
|
||||
|
||||
if (value) {
|
||||
this.setFlags(spriteIndex, currentFlags | mask);
|
||||
} else {
|
||||
this.setFlags(spriteIndex, currentFlags & ~mask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all sprite data.
|
||||
*/
|
||||
clear() {
|
||||
this.pixels.fill(0);
|
||||
this.flags.fill(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes sprite sheet to JSON.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
pixels: Array.from(this.pixels),
|
||||
flags: Array.from(this.flags)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates sprite sheet from JSON data.
|
||||
*
|
||||
* @param {Object} data Serialized sprite data.
|
||||
* @returns {SpriteSheet}
|
||||
*/
|
||||
static fromJSON(data) {
|
||||
return new SpriteSheet({
|
||||
pixels: new Uint8Array(data.pixels || []),
|
||||
flags: new Uint8Array(data.flags || [])
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports to PICO-8 .p8 format gfx section.
|
||||
*
|
||||
* @returns {string} Hex string for __gfx__ section.
|
||||
*/
|
||||
toP8Gfx() {
|
||||
const lines = [];
|
||||
|
||||
for (let row = 0; row < 128; row++) {
|
||||
let line = "";
|
||||
for (let col = 0; col < 128; col += 2) {
|
||||
const p1 = this.getPixel(col, row);
|
||||
const p2 = this.getPixel(col + 1, row);
|
||||
line += p1.toString(16) + p2.toString(16);
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports from PICO-8 .p8 format gfx section.
|
||||
*
|
||||
* @param {string} gfxData Hex string from __gfx__ section.
|
||||
*/
|
||||
fromP8Gfx(gfxData) {
|
||||
const lines = gfxData.trim().split("\n");
|
||||
|
||||
for (let row = 0; row < 128 && row < lines.length; row++) {
|
||||
const line = lines[row].trim();
|
||||
for (let col = 0; col < 128; col += 2) {
|
||||
const hexPair = line.substring(col, col + 2);
|
||||
if (hexPair.length === 2) {
|
||||
this.setPixel(col, row, parseInt(hexPair[0], 16));
|
||||
this.setPixel(col + 1, row, parseInt(hexPair[1], 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports sprite flags to PICO-8 .p8 format.
|
||||
*
|
||||
* @returns {string} Hex string for __gff__ section.
|
||||
*/
|
||||
toP8Flags() {
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < 256; i += 2) {
|
||||
const f1 = this.getFlags(i).toString(16).padStart(2, "0");
|
||||
const f2 = this.getFlags(i + 1).toString(16).padStart(2, "0");
|
||||
lines.push(f1 + f2);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports sprite flags from PICO-8 .p8 format.
|
||||
*
|
||||
* @param {string} flagData Hex string from __gff__ section.
|
||||
*/
|
||||
fromP8Flags(flagData) {
|
||||
const lines = flagData.trim().split("\n");
|
||||
|
||||
for (let i = 0; i < 128 && i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.length >= 2) {
|
||||
this.setFlags(i * 2, parseInt(line.substring(0, 2), 16));
|
||||
}
|
||||
if (line.length >= 4) {
|
||||
this.setFlags(i * 2 + 1, parseInt(line.substring(2, 4), 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
scripts/core/TileSet.js
Normal file
167
scripts/core/TileSet.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Represents a single tileset with 8x8 tiles (128x128px total, 16x16 grid).
|
||||
*/
|
||||
export class TileSet {
|
||||
/**
|
||||
* @param {Object} options Configuration options.
|
||||
* @param {string} options.id Unique identifier.
|
||||
* @param {string} options.name Display name.
|
||||
* @param {string} options.filePath Path or URL to PNG file (file:// or blob:).
|
||||
* @param {string} [options.nativePath] Native file system path (for Electron).
|
||||
* @param {number} [options.tileWidth=8] Tile width in pixels.
|
||||
* @param {number} [options.tileHeight=8] Tile height in pixels.
|
||||
* @param {number} [options.columns=16] Number of tiles per row (128px / 8px = 16).
|
||||
* @param {number} [options.rows=16] Number of tile rows (128px / 8px = 16).
|
||||
*/
|
||||
constructor(options) {
|
||||
this.id = options.id || this.#generateId();
|
||||
this.name = options.name || "Untitled Tileset";
|
||||
this.filePath = options.filePath || "";
|
||||
this.nativePath = options.nativePath || "";
|
||||
this.tileWidth = options.tileWidth || 8;
|
||||
this.tileHeight = options.tileHeight || 8;
|
||||
this.columns = options.columns || 16;
|
||||
this.rows = options.rows || 16;
|
||||
|
||||
/** @type {HTMLImageElement | null} */
|
||||
this.image = null;
|
||||
|
||||
/** @type {boolean} */
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique identifier.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
#generateId() {
|
||||
return `tileset_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of tiles.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getTileCount() {
|
||||
return this.columns * this.rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total width of the tileset image.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getWidth() {
|
||||
return this.columns * this.tileWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total height of the tileset image.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getHeight() {
|
||||
return this.rows * this.tileHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the tileset image from file path.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async load() {
|
||||
if (!this.filePath) {
|
||||
throw new Error("No file path specified");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
// Validate image is exactly 128x128px
|
||||
if (img.width !== 128 || img.height !== 128) {
|
||||
reject(new Error(`Tileset must be exactly 128x128px (found ${img.width}x${img.height}px)`));
|
||||
return;
|
||||
}
|
||||
|
||||
this.image = img;
|
||||
this.isLoaded = true;
|
||||
|
||||
// Fixed grid: 16x16 tiles for 128x128 image with 8x8 tiles
|
||||
this.columns = 16;
|
||||
this.rows = 16;
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error(`Failed to load image: ${this.filePath}`));
|
||||
};
|
||||
|
||||
// Add cache-busting for file:// URLs to ensure reload works
|
||||
const cacheBuster = this.filePath.startsWith("file://")
|
||||
? `?t=${Date.now()}`
|
||||
: "";
|
||||
img.src = this.filePath + cacheBuster;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the tileset image.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reload() {
|
||||
this.isLoaded = false;
|
||||
this.image = null;
|
||||
return this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tile coordinates for a specific tile index.
|
||||
*
|
||||
* @param {number} tileIndex Tile index (0-based).
|
||||
* @returns {{x: number, y: number, width: number, height: number}}
|
||||
*/
|
||||
getTileRect(tileIndex) {
|
||||
const col = tileIndex % this.columns;
|
||||
const row = Math.floor(tileIndex / this.columns);
|
||||
|
||||
return {
|
||||
x: col * this.tileWidth,
|
||||
y: row * this.tileHeight,
|
||||
width: this.tileWidth,
|
||||
height: this.tileHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes tileset to JSON.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
filePath: this.filePath,
|
||||
nativePath: this.nativePath,
|
||||
tileWidth: this.tileWidth,
|
||||
tileHeight: this.tileHeight,
|
||||
columns: this.columns,
|
||||
rows: this.rows
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tileset from JSON data.
|
||||
*
|
||||
* @param {Object} data Serialized tileset.
|
||||
* @returns {TileSet}
|
||||
*/
|
||||
static fromJSON(data) {
|
||||
return new TileSet(data);
|
||||
}
|
||||
}
|
||||
41
scripts/core/lua/LuaLiteralBoolean.js
Normal file
41
scripts/core/lua/LuaLiteralBoolean.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||
*/
|
||||
|
||||
/** @type {LuaLiteralHandler} */
|
||||
export const LuaLiteralBoolean = {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
format(value) {
|
||||
if (typeof value === "string") {
|
||||
return value === "false" ? "false" : "true";
|
||||
}
|
||||
return value ? "true" : "false";
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
defaultLiteral() {
|
||||
return "false";
|
||||
},
|
||||
|
||||
spawnableByDefault: true,
|
||||
|
||||
nodeModule: {
|
||||
definition: {
|
||||
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 }],
|
||||
},
|
||||
behavior: {
|
||||
evaluateValue: ({ node, formatLiteral }) =>
|
||||
formatLiteral("boolean", node.properties.value ?? true),
|
||||
},
|
||||
},
|
||||
};
|
||||
39
scripts/core/lua/LuaLiteralColor.js
Normal file
39
scripts/core/lua/LuaLiteralColor.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||
*/
|
||||
|
||||
/** @type {LuaLiteralHandler} */
|
||||
export const LuaLiteralColor = {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
format(value) {
|
||||
const n = Math.max(0, Math.min(15, Math.floor(Number(value) || 0)));
|
||||
return String(n);
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
defaultLiteral() {
|
||||
return "7";
|
||||
},
|
||||
|
||||
spawnableByDefault: true,
|
||||
|
||||
nodeModule: {
|
||||
definition: {
|
||||
id: "color_literal",
|
||||
title: "Color",
|
||||
category: "Values",
|
||||
description: "Constant color literal (0-15).",
|
||||
searchTags: ["color", "literal", "constant", "value"],
|
||||
inputs: [],
|
||||
outputs: [{ id: "value", name: "Value", direction: "output", kind: "color" }],
|
||||
properties: [{ key: "value", label: "Color", type: "number", defaultValue: 7 }],
|
||||
},
|
||||
behavior: {
|
||||
evaluateValue: ({ node, formatLiteral }) =>
|
||||
formatLiteral("color", node.properties.value ?? 7),
|
||||
},
|
||||
},
|
||||
};
|
||||
39
scripts/core/lua/LuaLiteralNumber.js
Normal file
39
scripts/core/lua/LuaLiteralNumber.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||
*/
|
||||
|
||||
/** @type {LuaLiteralHandler} */
|
||||
export const LuaLiteralNumber = {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
format(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? String(numeric) : "0";
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
defaultLiteral() {
|
||||
return "0";
|
||||
},
|
||||
|
||||
spawnableByDefault: true,
|
||||
|
||||
nodeModule: {
|
||||
definition: {
|
||||
id: "number_literal",
|
||||
title: "Number",
|
||||
category: "Values",
|
||||
description: "Constant numeric literal.",
|
||||
searchTags: ["number", "literal", "constant", "value"],
|
||||
inputs: [],
|
||||
outputs: [{ id: "value", name: "Value", direction: "output", kind: "number" }],
|
||||
properties: [{ key: "value", label: "Number", type: "number", defaultValue: 0 }],
|
||||
},
|
||||
behavior: {
|
||||
evaluateValue: ({ node, formatLiteral }) =>
|
||||
formatLiteral("number", node.properties.value ?? 0),
|
||||
},
|
||||
},
|
||||
};
|
||||
38
scripts/core/lua/LuaLiteralString.js
Normal file
38
scripts/core/lua/LuaLiteralString.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||
*/
|
||||
|
||||
/** @type {LuaLiteralHandler} */
|
||||
export const LuaLiteralString = {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
format(value) {
|
||||
return JSON.stringify(String(value ?? ""));
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
defaultLiteral() {
|
||||
return '""';
|
||||
},
|
||||
|
||||
spawnableByDefault: true,
|
||||
|
||||
nodeModule: {
|
||||
definition: {
|
||||
id: "string_literal",
|
||||
title: "String",
|
||||
category: "Values",
|
||||
description: "Constant string literal.",
|
||||
searchTags: ["string", "literal", "text", "value"],
|
||||
inputs: [],
|
||||
outputs: [{ id: "value", name: "Value", direction: "output", kind: "string" }],
|
||||
properties: [{ key: "value", label: "Text", type: "string", defaultValue: "hello" }],
|
||||
},
|
||||
behavior: {
|
||||
evaluateValue: ({ node, formatLiteral }) =>
|
||||
formatLiteral("string", node.properties.value ?? ""),
|
||||
},
|
||||
},
|
||||
};
|
||||
27
scripts/core/lua/LuaLiteralTable.js
Normal file
27
scripts/core/lua/LuaLiteralTable.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @typedef {import('./LuaLiteralTypes.js').LuaLiteralHandler} LuaLiteralHandler
|
||||
*/
|
||||
|
||||
/** @type {LuaLiteralHandler} */
|
||||
export const LuaLiteralTable = {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string}
|
||||
*/
|
||||
format(value) {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : "{}";
|
||||
}
|
||||
return "{}";
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
defaultLiteral() {
|
||||
return "{}";
|
||||
},
|
||||
|
||||
spawnableByDefault: false,
|
||||
|
||||
nodeModule: null,
|
||||
};
|
||||
56
scripts/core/lua/LuaLiteralTypes.js
Normal file
56
scripts/core/lua/LuaLiteralTypes.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { LuaLiteralNumber } from "./LuaLiteralNumber.js";
|
||||
import { LuaLiteralBoolean } from "./LuaLiteralBoolean.js";
|
||||
import { LuaLiteralString } from "./LuaLiteralString.js";
|
||||
import { LuaLiteralTable } from "./LuaLiteralTable.js";
|
||||
import { LuaLiteralColor } from "./LuaLiteralColor.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} LuaLiteralHandler
|
||||
* @property {(value: unknown) => string} format Formats a raw value into a Lua literal string.
|
||||
* @property {() => string} defaultLiteral Returns the default Lua literal string for this type.
|
||||
* @property {boolean} [spawnableByDefault] Whether this type creates a spawnable literal node in the graph.
|
||||
* @property {import('../../nodes/nodeTypes.js').NodeModule | null} [nodeModule] Node module definition, or null if no literal node exists.
|
||||
*/
|
||||
|
||||
/** @type {Map<string, LuaLiteralHandler>} */
|
||||
const HANDLERS = new Map([
|
||||
["number", LuaLiteralNumber],
|
||||
["boolean", LuaLiteralBoolean],
|
||||
["string", LuaLiteralString],
|
||||
["table", LuaLiteralTable],
|
||||
["color", LuaLiteralColor],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Formats a raw value into a Lua literal string for the given pin kind.
|
||||
*
|
||||
* @param {string} kind Pin kind.
|
||||
* @param {unknown} value Raw value.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatLiteral(kind, value) {
|
||||
const handler = HANDLERS.get(kind);
|
||||
return handler ? handler.format(value) : LuaLiteralString.format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default Lua literal string for the given pin kind.
|
||||
*
|
||||
* @param {string} kind Pin kind.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function defaultLiteralForKind(kind) {
|
||||
const handler = HANDLERS.get(kind);
|
||||
return handler ? handler.defaultLiteral() : "nil";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all node modules defined by literal handlers that have a nodeModule.
|
||||
*
|
||||
* @returns {Array<import('../../nodes/nodeTypes.js').NodeModule>}
|
||||
*/
|
||||
export function getLiteralNodeModules() {
|
||||
return [...HANDLERS.values()]
|
||||
.map((handler) => handler.nodeModule)
|
||||
.filter(Boolean);
|
||||
}
|
||||
Reference in New Issue
Block a user