diff --git a/.gitignore b/.gitignore index d29de0c7..194189d3 100644 --- a/.gitignore +++ b/.gitignore @@ -83,7 +83,6 @@ assets/borrowed .VSCode* /vita - ._* *~ @@ -105,4 +104,5 @@ yarn.lock /build2 /build* -/assets/test \ No newline at end of file +/assets/test +/tools_old \ No newline at end of file diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index ff8c5acb..97d63a0a 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -58,9 +58,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME} main.c ) -# Defs -dusk_env_to_h(duskdefs.env duskdefs.h) - # Subdirs add_subdirectory(assert) add_subdirectory(asset) diff --git a/src/dusk/display/CMakeLists.txt b/src/dusk/display/CMakeLists.txt index a2246817..2597fc08 100644 --- a/src/dusk/display/CMakeLists.txt +++ b/src/dusk/display/CMakeLists.txt @@ -21,7 +21,7 @@ add_subdirectory(texture) # Color definitions dusk_run_python( dusk_color_defs - tools.display.color.csv + tools.color.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h ) diff --git a/src/dusk/duskdefs.env b/src/dusk/duskdefs.env deleted file mode 100644 index 1629af99..00000000 --- a/src/dusk/duskdefs.env +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -ENTITY_DIR_SOUTH = 0 -ENTITY_DIR_WEST = 1 -ENTITY_DIR_EAST = 2 -ENTITY_DIR_NORTH = 3 - -ENTITY_COUNT = 128 - -ENTITY_TYPE_NULL = 0 -ENTITY_TYPE_PLAYER = 1 -ENTITY_TYPE_NPC = 2 -ENTITY_TYPE_COUNT = 3 - -CHUNK_WIDTH = 16 -CHUNK_HEIGHT = 16 -CHUNK_DEPTH = 4 -# CHUNK_VERTEX_COUNT_MAX = QUAD_VERTEXES * CHUNK_WIDTH * CHUNK_HEIGHT * 4 -CHUNK_VERTEX_COUNT_MAX=6144 -CHUNK_MESH_COUNT_MAX = 14 -CHUNK_ENTITY_COUNT_MAX = 8 - -TILE_WIDTH = 16.0 -TILE_HEIGHT = 16.0 -TILE_DEPTH = 16.0 - -TILE_SHAPE_NULL = 0 -TILE_SHAPE_FLOOR = 1 -TILE_SHAPE_RAMP_SOUTH = 2 -TILE_SHAPE_RAMP_WEST = 3 -TILE_SHAPE_RAMP_EAST = 4 -TILE_SHAPE_RAMP_NORTH = 5 -TILE_SHAPE_RAMP_SOUTHWEST = 6 -TILE_SHAPE_RAMP_SOUTHEAST = 7 -TILE_SHAPE_RAMP_NORTHWEST = 8 -TILE_SHAPE_RAMP_NORTHEAST = 9 - -RPG_CAMERA_FOV = 70 -RPG_CAMERA_PIXELS_PER_UNIT = 1.0 -RPG_CAMERA_Z_OFFSET = 24.0 - -ASSET_LANG_CHUNK_CHAR_COUNT = 6144 \ No newline at end of file diff --git a/src/duskrpg/story/CMakeLists.txt b/src/duskrpg/story/CMakeLists.txt index 880548a3..2687e613 100644 --- a/src/duskrpg/story/CMakeLists.txt +++ b/src/duskrpg/story/CMakeLists.txt @@ -14,6 +14,6 @@ dusk_run_python( dusk_story_defs tools.story.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/storyflag.csv - --header-file ${DUSK_GENERATED_HEADERS_DIR}/story/storyflagvalue.h + --output ${DUSK_GENERATED_HEADERS_DIR}/story/storyflagvalue.h ) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_story_defs) \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index fc27de78..fa3f544a 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,7 +1,6 @@ -# Copyright (c) 2025 Dominic Msters -# +# Copyright (c) 2026 Dominic Masters +# # This software is released under the MIT License. # https://opensource.org/licenses/MIT -add_subdirectory(run_python) -add_subdirectory(env_to_h) \ No newline at end of file +add_subdirectory(run_python) \ No newline at end of file diff --git a/tools/color/csv/__main__.py b/tools/color/csv/__main__.py new file mode 100644 index 00000000..a86836db --- /dev/null +++ b/tools/color/csv/__main__.py @@ -0,0 +1,79 @@ +import argparse +import csv +import os + +parser = argparse.ArgumentParser(description="Color CSV to .h defines") +parser.add_argument("--csv", required=True, help="Path to color CSV file") +parser.add_argument("--output", required=True, help="Path to output .h file") +args = parser.parse_args() + +# Load colors +colors = {} +with open(args.csv, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + if "name" not in reader.fieldnames: + raise ValueError("CSV must have a 'name' column") + for row in reader: + name = row["name"] + r, g, b = row["r"], row["g"], row["b"] + a = row["a"] if "a" in row and row["a"] != "" else "1.0" + for ch in (r, g, b, a): + if not (0.0 <= float(ch) <= 1.0): + raise ValueError(f"Color channel {ch!r} for '{name}' out of range [0.0, 1.0]") + colors[name] = (r, g, b, a) + +# Build output +out = [ + "#pragma once", + '#include "dusk.h"', + "", + "typedef float_t colorchannelf_t;", + "typedef uint8_t colorchannel8_t;", + "", + "typedef struct { colorchannelf_t r, g, b; } color3f_t;", + "typedef struct { colorchannelf_t r, g, b, a; } color4f_t;", + "typedef struct { colorchannel8_t r, g, b; } color3b_t;", + "typedef struct { colorchannel8_t r, g, b, a; } color4b_t;", + "typedef color4b_t color_t;", + "", + "#define color3f(r, g, b) ((color3f_t){ r, g, b })", + "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })", + "#define color3b(r, g, b) ((color3b_t){ r, g, b })", + "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })", + "#define color(r, g, b, a) color4b(r, g, b, a)", + "#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))", + "", +] + +lua = [] +for name, (r, g, b, a) in colors.items(): + r8, g8, b8, a8 = (int(float(ch) * 255) for ch in (r, g, b, a)) + macro = "COLOR_" + name.upper() + camel = "".join(p[0].upper() + p[1:].lower() for p in name.split("_")) + + out += [ + f"// {name}", + f"#define {macro}_4B color4b({r8}, {g8}, {b8}, {a8})", + f"#define {macro}_3B color3b({r8}, {g8}, {b8})", + f"#define {macro}_3F color3f({r}f, {g}f, {b}f)", + f"#define {macro}_4F color4f({r}f, {g}f, {b}f, {a}f)", + f"#define {macro} {macro}_4B", + "", + ] + lua += [ + f"function color{camel}()", + f" return color({r8}, {g8}, {b8}, {a8})", + "end", + "", + ] + +out.append("// Lua color functions") +out.append("#define COLOR_SCRIPT \\") +for line in "\n".join(lua).rstrip().splitlines(): + out.append(f' "{line}\\n" \\') +out[-1] = out[-1].rstrip(" \\") +out.append("") + +os.makedirs(os.path.dirname(args.output), exist_ok=True) +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out)) diff --git a/tools/display/color/csv/__main__.py b/tools/display/color/csv/__main__.py deleted file mode 100644 index 81017be3..00000000 --- a/tools/display/color/csv/__main__.py +++ /dev/null @@ -1,106 +0,0 @@ -import argparse -import os -import csv - -parser = argparse.ArgumentParser(description="Color CSV to .h defines") -parser.add_argument("--csv", required=True, help="Path to color CSV file") -parser.add_argument("--output", required=True, help="Path to output .h file") -args = parser.parse_args() - -def csvIdToEnumName(inputId): - return "INPUT_ACTION_" + inputId.upper() - -# Load up CSV file. -outHeader = "#pragma once\n" -outHeader += '#include "dusk.h"\n\n' - -outHeader += f"typedef float_t colorchannelf_t;\n" - -colors = {} - -with open(args.csv, newline="", encoding="utf-8") as csvfile: - reader = csv.DictReader(csvfile) - - # CSV must have id column. - if "name" not in reader.fieldnames: - raise Exception("CSV file must have 'name' column") - - # For each color, by name... - for row in reader: - name = row["name"] - r = row["r"] - g = row["g"] - b = row["b"] - a = row["a"] if "a" in row and row["a"] != "" else "1.0" - - # Ensure values are between 0.0 and 1.0 - for channelValue in (r, g, b, a): - fvalue = float(channelValue) - if fvalue < 0.0 or fvalue > 1.0: - raise Exception(f"Color channel value {channelValue} for color {name} is out of range (0.0 to 1.0)") - - colors[name] = (r, g, b, a) - -# Prep output header -outHeader = "#pragma once\n" -outHeader += '#include "dusk.h"\n\n' - -# Typedefs for float and uin8t color channels -outHeader += f"typedef float_t colorchannelf_t;\n" -outHeader += f"typedef uint8_t colorchannel8_t;\n\n" - -# Typedefs for 3 and 4 channel colors in both float and uint8_t -outHeader += f"typedef struct {{ colorchannelf_t r, g, b; }} color3f_t;\n" -outHeader += f"typedef struct {{ colorchannelf_t r, g, b, a; }} color4f_t;\n" -outHeader += f"typedef struct {{ colorchannel8_t r, g, b; }} color3b_t;\n" -outHeader += f"typedef struct {{ colorchannel8_t r, g, b, a; }} color4b_t;\n" -outHeader += "typedef color4b_t color_t;\n\n"# Preferred format. - -outHeader += "#define color3f(r, g, b) ((color3f_t){ r, g, b })\n" -outHeader += "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })\n" -outHeader += "#define color3b(r, g, b) ((color3b_t){ r, g, b })\n" -outHeader += "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })\n\n" -outHeader += "#define color(r, g, b, a) color4b(r, g, b, a)\n\n" # Preferred format. -outHeader += "#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))\n\n" - -luaScript = "" - -# Define each color, in each format -for color_name, (r, g, b, a) in colors.items(): - outHeader += f"// Color: {color_name}\n" - - r8 = int(float(r) * 255) - g8 = int(float(g) * 255) - b8 = int(float(b) * 255) - a8 = int(float(a) * 255) - - macro_name = "COLOR_" + color_name.upper() - - outHeader += f"#define {macro_name}_4B color4b({r8}, {g8}, {b8}, {a8})\n" - outHeader += f"#define {macro_name}_3B color3b({r8}, {g8}, {b8})\n" - outHeader += f"#define {macro_name}_3F color3f({r}f, {g}f, {b}f)\n" - outHeader += f"#define {macro_name}_4F color4f({r}f, {g}f, {b}f, {a}f)\n" - outHeader += f"#define {macro_name} {macro_name}_4B\n\n" # Preferred format. - - nameSplit = color_name.split("_") - camelFirstLetter = "" - for part in nameSplit: - camelFirstLetter += part[0].upper() + part[1:].lower() - - luaName = "color" + camelFirstLetter - - luaScript += f"function {luaName}()\n" - luaScript += f" return color({r8}, {g8}, {b8}, {a8})\n" - luaScript += "end\n\n" - -outHeader += "// Lua color functions\n" -outHeader += "#define COLOR_SCRIPT \\\n" -# Stringify and put each line with a backslash -for line in luaScript.splitlines(): - outHeader += f' "{line}\\n" \\\n' -outHeader = outHeader.rstrip(" \\\n") + "\n" # Remove last backslash and add newline - -# Write to output file -os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as outFile: - outFile.write(outHeader) \ No newline at end of file diff --git a/tools/editor/common/dtf.js b/tools/editor/common/dtf.js new file mode 100644 index 00000000..04c87574 --- /dev/null +++ b/tools/editor/common/dtf.js @@ -0,0 +1,173 @@ +"use strict"; + +// DTF – Dusk Texture Format +// +// Header (13 bytes): +// [0–2] "DTF" magic +// [3] 0x01 version +// [4–7] uint32 width (little-endian) +// [8–11] uint32 height (little-endian) +// [12] uint8 format +// +// Formats: +// 0x01 Alpha – 1 byte per pixel (alpha channel only) +// 0x03 RGB – 3 bytes per pixel (no alpha) +// 0x04 RGBA – 4 bytes per pixel +// +// Followed by width × height × bpp bytes of tightly-packed pixel data. + +const DTF = (() => { + const MAGIC = [0x44, 0x54, 0x46]; // "DTF" + const VERSION = 0x01; + const FORMAT_ALPHA = 0x01; + const FORMAT_RGB = 0x03; + const FORMAT_RGBA = 0x04; + const HEADER_SIZE = 13; + + // Bytes per pixel for each format. + const BPP = { + [FORMAT_ALPHA]: 1, + [FORMAT_RGB]: 3, + [FORMAT_RGBA]: 4, + }; + + // Encode RGBA source pixels into a DTF ArrayBuffer at the given format. + // When format is FORMAT_ALPHA and redAsAlpha is true, the red channel is + // used as the alpha value instead of the actual alpha channel. + function encode(width, height, rgbaData, format, redAsAlpha) { + if (format === undefined) format = FORMAT_RGBA; + const bpp = BPP[format]; + if (bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`); + + const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); + const buf = new ArrayBuffer(HEADER_SIZE + width * height * bpp); + const bytes = new Uint8Array(buf); + const view = new DataView(buf); + + bytes[0] = MAGIC[0]; + bytes[1] = MAGIC[1]; + bytes[2] = MAGIC[2]; + bytes[3] = VERSION; + view.setUint32(4, width, true); + view.setUint32(8, height, true); + bytes[12] = format; + + let dst = HEADER_SIZE; + for (let i = 0; i < width * height; i++) { + const o = i * 4; + switch (format) { + case FORMAT_ALPHA: + bytes[dst++] = redAsAlpha ? src[o] : src[o + 3]; + break; + case FORMAT_RGB: + bytes[dst++] = src[o]; + bytes[dst++] = src[o + 1]; + bytes[dst++] = src[o + 2]; + break; + default: // FORMAT_RGBA + bytes[dst++] = src[o]; + bytes[dst++] = src[o + 1]; + bytes[dst++] = src[o + 2]; + bytes[dst++] = src[o + 3]; + } + } + + return buf; + } + + // Decode a DTF ArrayBuffer. Always returns RGBA pixel data for internal use. + // Alpha-format files decode as {R=0, G=0, B=0, A=alpha} so the alpha channel + // is preserved and toImageData() can display it correctly. + function decode(buffer) { + const bytes = new Uint8Array(buffer); + const view = new DataView(buffer); + + if (bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF"); + if (bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) { + throw new Error("Invalid DTF magic bytes – not a DTF file"); + } + + const version = bytes[3]; + if (version !== VERSION) { + throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`); + } + + const width = view.getUint32(4, true); + const height = view.getUint32(8, true); + const format = bytes[12]; + const bpp = BPP[format]; + + if (bpp === undefined) { + throw new Error(`Unsupported DTF format: 0x${format.toString(16).padStart(2, "0")}`); + } + + const expected = HEADER_SIZE + width * height * bpp; + if (bytes.length < expected) { + throw new Error(`DTF pixel data truncated (expected ${expected} bytes, got ${bytes.length})`); + } + + const rgba = new Uint8ClampedArray(width * height * 4); + let src = HEADER_SIZE; + for (let i = 0; i < width * height; i++) { + const o = i * 4; + switch (format) { + case FORMAT_ALPHA: + rgba[o] = rgba[o + 1] = rgba[o + 2] = 0; + rgba[o + 3] = bytes[src++]; + break; + case FORMAT_RGB: + rgba[o] = bytes[src++]; + rgba[o + 1] = bytes[src++]; + rgba[o + 2] = bytes[src++]; + rgba[o + 3] = 255; + break; + default: // FORMAT_RGBA + rgba[o] = bytes[src++]; + rgba[o + 1] = bytes[src++]; + rgba[o + 2] = bytes[src++]; + rgba[o + 3] = bytes[src++]; + } + } + + return { width, height, format, data: rgba }; + } + + // Convert RGBA source pixels to a display-ready ImageData for the given format. + // Shows exactly how the texture will look after a DTF encode/decode round-trip. + // Alpha → grayscale from alpha channel (or red if redAsAlpha), out-alpha=255 + // RGB → discard alpha, fully opaque + // RGBA → pass-through + function toImageData(width, height, rgbaData, format, redAsAlpha) { + const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); + const out = new Uint8ClampedArray(width * height * 4); + + for (let i = 0; i < width * height; i++) { + const o = i * 4; + switch (format) { + case FORMAT_ALPHA: + out[o] = out[o + 1] = out[o + 2] = redAsAlpha ? src[o] : src[o + 3]; + out[o + 3] = 255; + break; + case FORMAT_RGB: + out[o] = src[o]; + out[o + 1] = src[o + 1]; + out[o + 2] = src[o + 2]; + out[o + 3] = 255; + break; + default: // FORMAT_RGBA + out[o] = src[o]; + out[o + 1] = src[o + 1]; + out[o + 2] = src[o + 2]; + out[o + 3] = src[o + 3]; + } + } + + return new ImageData(out, width, height); + } + + return Object.freeze({ + encode, decode, toImageData, + FORMAT_ALPHA, FORMAT_RGB, FORMAT_RGBA, + VERSION, HEADER_SIZE, BPP, + }); +})(); diff --git a/tools/editor/common/png.js b/tools/editor/common/png.js new file mode 100644 index 00000000..54d6100f --- /dev/null +++ b/tools/editor/common/png.js @@ -0,0 +1,55 @@ +"use strict"; + +// DuskPNG – PNG export using pngjs (window.PNG) +// Falls back to canvas.toBlob if pngjs is unavailable. + +const DuskPNG = (() => { + function _pngAvailable() { + return typeof PNG !== "undefined" && PNG.sync && typeof PNG.sync.write === "function"; + } + + // Encode RGBA pixel data into a PNG Buffer via pngjs. + // Returns a Uint8Array (Buffer) if pngjs is available, otherwise null. + function encode(width, height, rgbaData) { + if (!_pngAvailable()) return null; + + const png = new PNG({ width, height }); + const src = rgbaData instanceof Uint8ClampedArray + ? rgbaData + : new Uint8ClampedArray(rgbaData); + + for (let i = 0; i < src.length; i++) { + png.data[i] = src[i]; + } + + return PNG.sync.write(png); // returns a Buffer (Uint8Array subclass) + } + + // Trigger a browser download of the RGBA data as a PNG file. + function download(filename, width, height, rgbaData) { + const buf = encode(width, height, rgbaData); + + if (buf) { + // pngjs path + const blob = new Blob([buf], { type: "image/png" }); + _triggerDownload(URL.createObjectURL(blob), filename); + } else { + // Canvas fallback + const canvas = Object.assign(document.createElement("canvas"), { width, height }); + const ctx = canvas.getContext("2d"); + const src = rgbaData instanceof Uint8ClampedArray + ? rgbaData + : new Uint8ClampedArray(rgbaData); + ctx.putImageData(new ImageData(src, width, height), 0, 0); + canvas.toBlob(blob => _triggerDownload(URL.createObjectURL(blob), filename), "image/png"); + } + } + + function _triggerDownload(url, filename) { + const a = Object.assign(document.createElement("a"), { href: url, download: filename }); + a.click(); + URL.revokeObjectURL(url); + } + + return Object.freeze({ encode, download }); +})(); diff --git a/tools/editor/index.html b/tools/editor/index.html new file mode 100644 index 00000000..babf92c3 --- /dev/null +++ b/tools/editor/index.html @@ -0,0 +1,38 @@ + + + + + + Dusk Editor Tools + + + + + + +
+ +
+

Editor Tools

+

Asset creation and data authoring tools for the Dusk game engine.

+
+ +
+ + +
+ +
+ + + diff --git a/tools/editor/styles/components/buttons.css b/tools/editor/styles/components/buttons.css new file mode 100644 index 00000000..b3ce4bfe --- /dev/null +++ b/tools/editor/styles/components/buttons.css @@ -0,0 +1,34 @@ +/* Component – Buttons */ + +.btn { + display: block; + width: 100%; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + padding: 0.4rem 0.75rem; + font-size: 0.825rem; + cursor: pointer; + text-align: center; + font-family: inherit; + transition: background var(--speed), border-color var(--speed); +} + +.btn + .btn { + margin-top: 0.375rem; +} + +.btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.btn-primary { + background: var(--accent-dim); + border-color: var(--accent); +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent); +} diff --git a/tools/editor/styles/components/cards.css b/tools/editor/styles/components/cards.css new file mode 100644 index 00000000..1f5e6efe --- /dev/null +++ b/tools/editor/styles/components/cards.css @@ -0,0 +1,38 @@ +/* Component – Tool cards and empty state */ + +.tool-card { + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.4rem; + transition: border-color var(--speed); + text-decoration: none; + color: inherit; +} + +.tool-card:hover { + text-decoration: none; +} + +.tool-card .tool-name { + font-weight: 600; + font-size: 0.95rem; +} + +.tool-card .tool-desc { + color: var(--text-muted); + font-size: 0.85rem; + line-height: 1.5; +} + +.empty-state { + border-style: dashed; + padding: 3rem; + text-align: center; + color: var(--text-muted); +} + +.empty-state p { + margin-top: 0.5rem; + font-size: 0.875rem; +} diff --git a/tools/editor/styles/components/file-info.css b/tools/editor/styles/components/file-info.css new file mode 100644 index 00000000..d9d14399 --- /dev/null +++ b/tools/editor/styles/components/file-info.css @@ -0,0 +1,43 @@ +/* Component – File info and load area */ + +.file-name { + font-size: 0.775rem; + color: var(--text-muted); + word-break: break-all; + line-height: 1.4; +} + +.info-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8rem; +} + +.info-table td { + padding: 0.15rem 0; + vertical-align: top; +} + +.info-table td:first-child { + color: var(--text-muted); + width: 50px; + font-size: 0.75rem; + padding-right: 0.75rem; +} + +.load-area { + border: 1px dashed var(--border); + border-radius: var(--radius); + padding: 0.6rem; + text-align: center; + cursor: pointer; + font-size: 0.825rem; + color: var(--text-muted); + transition: border-color var(--speed), color var(--speed); + line-height: 1.5; +} + +.load-area:hover, +.load-area.drag-over { + color: var(--text); +} diff --git a/tools/editor/styles/components/header.css b/tools/editor/styles/components/header.css new file mode 100644 index 00000000..b9f8ff85 --- /dev/null +++ b/tools/editor/styles/components/header.css @@ -0,0 +1,43 @@ +/* Component – Site header */ + +.site-header { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + padding: 0 var(--gap); + display: flex; + align-items: center; + gap: 1rem; + height: 52px; +} + +.site-header .logo { + font-weight: 700; + font-size: 1rem; + color: var(--text); + letter-spacing: 0.03em; +} + +.site-header .logo span { + color: var(--accent); +} + +.site-header nav { + display: flex; + gap: 0.25rem; + margin-left: auto; +} + +.site-header nav a { + padding: 0.3rem 0.75rem; + border-radius: var(--radius); + color: var(--text-muted); + font-size: 0.875rem; + transition: background var(--speed), color var(--speed); +} + +.site-header nav a:hover, +.site-header nav a.active { + background: var(--bg-raised); + color: var(--text); + text-decoration: none; +} diff --git a/tools/editor/styles/components/hero.css b/tools/editor/styles/components/hero.css new file mode 100644 index 00000000..b5fb9645 --- /dev/null +++ b/tools/editor/styles/components/hero.css @@ -0,0 +1,16 @@ +/* Component – Hero section */ + +.hero { + padding: 2.5rem 0 2rem; +} + +.hero h1 { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.hero p { + color: var(--text-muted); + max-width: 540px; +} diff --git a/tools/editor/styles/components/panel.css b/tools/editor/styles/components/panel.css new file mode 100644 index 00000000..06180413 --- /dev/null +++ b/tools/editor/styles/components/panel.css @@ -0,0 +1,79 @@ +/* Component – Tool panel (sidebar) */ + +.tool-panel { + width: 210px; + flex-shrink: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.panel-section { + padding: 0.875rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.panel-section:last-child { + border-bottom: none; +} + +.control-row { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.control-row label { + color: var(--text-muted); + min-width: 44px; +} + +.tool-panel select { + width: 100%; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 0.25rem 0.4rem; + font-size: 0.875rem; + font-family: inherit; + transition: border-color var(--speed); + cursor: pointer; +} + +.tool-panel select:focus { + outline: none; + border-color: var(--accent-dim); +} + +.tool-panel input[type="checkbox"] { + accent-color: var(--accent); + cursor: pointer; + flex-shrink: 0; +} + +.tool-panel input[type="number"] { + width: 52px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 0.25rem 0.4rem; + font-size: 0.875rem; + font-family: inherit; + transition: border-color var(--speed); +} + +.tool-panel input[type="number"]:focus { + outline: none; + border-color: var(--accent-dim); +} + +.unit { + color: var(--text-muted); + font-size: 0.875rem; +} diff --git a/tools/editor/styles/components/preview.css b/tools/editor/styles/components/preview.css new file mode 100644 index 00000000..c5b7a8de --- /dev/null +++ b/tools/editor/styles/components/preview.css @@ -0,0 +1,36 @@ +/* Component – Preview area */ + +.tool-preview { + flex: 1; + position: relative; + min-height: 300px; + transition: border-color var(--speed); +} + +.preview-scroll { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.preview-scroll canvas { + image-rendering: pixelated; + image-rendering: crisp-edges; + display: block; + max-width: none; +} + +.preview-empty { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-muted); + font-size: 0.875rem; + text-align: center; + white-space: nowrap; + user-select: none; + pointer-events: none; +} diff --git a/tools/editor/styles/components/swatches.css b/tools/editor/styles/components/swatches.css new file mode 100644 index 00000000..e80b896b --- /dev/null +++ b/tools/editor/styles/components/swatches.css @@ -0,0 +1,28 @@ +/* Component – Background swatches */ + +.bg-swatches { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.bg-swatch { + width: 26px; + height: 26px; + border-radius: var(--radius-sm); + border: 2px solid transparent; + cursor: pointer; + padding: 0; + flex-shrink: 0; + overflow: hidden; + transition: border-color var(--speed); +} + +.bg-swatch:hover { + border-color: var(--text-muted); +} + +.bg-swatch.active, +.bg-swatch.active:hover { + border-color: var(--accent); +} diff --git a/tools/editor/styles/components/warnings.css b/tools/editor/styles/components/warnings.css new file mode 100644 index 00000000..59e5fc88 --- /dev/null +++ b/tools/editor/styles/components/warnings.css @@ -0,0 +1,28 @@ +/* Component – Warnings panel */ + +.warnings-section { + border-color: #6b4a00; +} + +.warning-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.warning-list li { + font-size: 0.775rem; + color: #e8a030; + line-height: 1.4; + padding-left: 1.1rem; + position: relative; +} + +.warning-list li::before { + content: "!"; + position: absolute; + left: 0; + font-weight: 700; + color: #e8a030; +} diff --git a/tools/editor/styles/elements.css b/tools/editor/styles/elements.css new file mode 100644 index 00000000..afc29db7 --- /dev/null +++ b/tools/editor/styles/elements.css @@ -0,0 +1,19 @@ +/* Elements – Bare HTML */ + +body { + background: var(--bg); + color: var(--text); + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.6; + min-height: 100vh; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/tools/editor/styles/generic.css b/tools/editor/styles/generic.css new file mode 100644 index 00000000..4d67dbe3 --- /dev/null +++ b/tools/editor/styles/generic.css @@ -0,0 +1,7 @@ +/* Generic – Reset */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} diff --git a/tools/editor/styles/main.css b/tools/editor/styles/main.css new file mode 100644 index 00000000..0b52c034 --- /dev/null +++ b/tools/editor/styles/main.css @@ -0,0 +1,15 @@ +/* ITCSS entry point */ + +@import "settings.css"; +@import "generic.css"; +@import "elements.css"; +@import "objects.css"; +@import "components/header.css"; +@import "components/hero.css"; +@import "components/cards.css"; +@import "components/panel.css"; +@import "components/buttons.css"; +@import "components/swatches.css"; +@import "components/preview.css"; +@import "components/file-info.css"; +@import "components/warnings.css"; diff --git a/tools/editor/styles/objects.css b/tools/editor/styles/objects.css new file mode 100644 index 00000000..9c5c8752 --- /dev/null +++ b/tools/editor/styles/objects.css @@ -0,0 +1,59 @@ +/* Objects – Layout patterns and shared abstractions */ + +/* Surface base – background, border, and radius shared across raised UI */ +.tool-card, +.tool-panel, +.tool-preview, +.empty-state { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +/* Hover accent border – interactive surfaces that highlight on focus/drag */ +.tool-card:hover, +.btn:hover:not(:disabled), +.load-area:hover, +.load-area.drag-over, +.tool-preview.drag-over { + border-color: var(--accent-dim); +} + +/* Section label – small uppercase heading shared across page and panel contexts */ +.section-label { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +/* Page container */ +.page { + max-width: 1400px; + margin: 0 auto; + padding: var(--gap); +} + +/* Top-level sections share uniform vertical spacing */ +.section, +.tool-workspace { + margin-top: var(--gap); +} + +.section > .section-label { + margin-bottom: 1rem; +} + +/* Tool grid */ +.tool-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 1rem; +} + +/* Tool workspace */ +.tool-workspace { + display: flex; + gap: 1rem; +} diff --git a/tools/editor/styles/settings.css b/tools/editor/styles/settings.css new file mode 100644 index 00000000..7d9c31ff --- /dev/null +++ b/tools/editor/styles/settings.css @@ -0,0 +1,16 @@ +/* Settings – Design tokens */ + +:root { + --bg: #1a1a1e; + --bg-surface: #25252b; + --bg-raised: #2e2e36; + --border: #3a3a44; + --text: #e4e4ed; + --text-muted: #7a7a90; + --accent: #6e8efb; + --accent-dim: #3a4a8a; + --radius: 6px; + --radius-sm: 4px; + --gap: 1.5rem; + --speed: 0.15s; +} diff --git a/tools/editor/texture/index.html b/tools/editor/texture/index.html new file mode 100644 index 00000000..32b6e051 --- /dev/null +++ b/tools/editor/texture/index.html @@ -0,0 +1,124 @@ + + + + + + Texture Creator – Dusk Editor + + + + + + +
+ +
+

Texture Creator

+

Load an image or existing .dtf file, then export as Dusk Texture Format (.dtf) or PNG.

+
+ +
+ + + + + +
+
Load an image or .dtf file to get started
+ +
+ +
+
+ + + + + + + diff --git a/tools/editor/texture/texture.js b/tools/editor/texture/texture.js new file mode 100644 index 00000000..93011fde --- /dev/null +++ b/tools/editor/texture/texture.js @@ -0,0 +1,278 @@ +"use strict"; + +// ─── State ─────────────────────────────────────────────────────────────────── + +const state = { + pixels: null, // Uint8ClampedArray RGBA at original resolution + width: 0, + height: 0, + scale: 1, + bg: "grid", + format: DTF.FORMAT_RGBA, // output DTF format + redAsAlpha: false, // Alpha format: use red channel instead of alpha + filename: "texture", // basename without extension +}; + +// ─── DOM refs ──────────────────────────────────────────────────────────────── + +const canvas = document.getElementById("canvas"); +const ctx = canvas.getContext("2d"); +const fileInput = document.getElementById("file-input"); +const loadLabel = document.getElementById("load-label"); +const scaleInput = document.getElementById("scale-input"); +const bgSwatches = document.getElementById("bg-swatches"); +const previewArea = document.getElementById("preview-area"); +const previewEmpty = document.getElementById("preview-empty"); +const previewScroll = document.getElementById("preview-scroll"); +const infoSection = document.getElementById("info-section"); +const fileNameEl = document.getElementById("file-name"); +const infoFilename = document.getElementById("info-filename"); +const infoSize = document.getElementById("info-size"); +const infoFormat = document.getElementById("info-format"); +const btnDtf = document.getElementById("btn-dtf"); +const btnPng = document.getElementById("btn-png"); +const formatSelect = document.getElementById("format-select"); +const infoDtfSize = document.getElementById("info-dtf-size"); +const warningsSection = document.getElementById("warnings-section"); +const warningList = document.getElementById("warning-list"); +const redAsAlphaRow = document.getElementById("red-as-alpha-row"); +const redAsAlphaCheck = document.getElementById("red-as-alpha"); + +// ─── Rendering ─────────────────────────────────────────────────────────────── + +const CHECKER_CELL = 8; + +function drawCheckerboard(w, h) { + for (let y = 0; y < h; y += CHECKER_CELL) { + for (let x = 0; x < w; x += CHECKER_CELL) { + ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff"; + ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y)); + } + } +} + +function render() { + if (!state.pixels) return; + + const { pixels, width, height, scale, bg, format } = state; + const cw = width * scale; + const ch = height * scale; + + canvas.width = cw; + canvas.height = ch; + + // 1. Background + if (bg === "grid") { + drawCheckerboard(cw, ch); + } else { + ctx.fillStyle = bg; + ctx.fillRect(0, 0, cw, ch); + } + + // 2. Composite image on top at scaled size (pixelated) + const src = Object.assign(document.createElement("canvas"), { width, height }); + src.getContext("2d").putImageData(DTF.toImageData(width, height, pixels, format, state.redAsAlpha), 0, 0); + + ctx.imageSmoothingEnabled = false; + ctx.drawImage(src, 0, 0, cw, ch); +} + +// ─── Loading ───────────────────────────────────────────────────────────────── + +function loadStandardImage(file) { + const url = URL.createObjectURL(file); + const img = new Image(); + + img.onload = () => { + const offscreen = Object.assign(document.createElement("canvas"), { + width: img.naturalWidth, + height: img.naturalHeight, + }); + offscreen.getContext("2d").drawImage(img, 0, 0); + const imageData = offscreen.getContext("2d").getImageData(0, 0, img.naturalWidth, img.naturalHeight); + + applyImageData( + img.naturalWidth, + img.naturalHeight, + imageData.data, + file.name, + file.type || "image", + ); + URL.revokeObjectURL(url); + }; + + img.onerror = () => { + showError("Failed to load image. The file may be corrupt or unsupported."); + URL.revokeObjectURL(url); + }; + + img.src = url; +} + +function loadDTF(file) { + const reader = new FileReader(); + reader.onload = e => { + try { + const { width, height, format, data } = DTF.decode(e.target.result); + const label = format === DTF.FORMAT_ALPHA ? "DTF (Alpha)" + : format === DTF.FORMAT_RGB ? "DTF (RGB)" + : "DTF (RGBA)"; + applyImageData(width, height, data, file.name, label, format); + } catch (err) { + showError(`Failed to load DTF: ${err.message}`); + } + }; + reader.readAsArrayBuffer(file); +} + +function updateWarnings() { + const warnings = []; + + if (state.pixels) { + const { width, height } = state; + const isPow2 = n => n > 0 && (n & (n - 1)) === 0; + + if (width < 4) warnings.push(`Width is below 4 px (${width})`); + if (height < 4) warnings.push(`Height is below 4 px (${height})`); + if (!isPow2(width)) warnings.push(`Width is not a power of two (${width})`); + if (!isPow2(height)) warnings.push(`Height is not a power of two (${height})`); + + const bytes = DTF.HEADER_SIZE + width * height * DTF.BPP[state.format]; + if (bytes > 256 * 1024) { + warnings.push(`Output exceeds 256 KB (${(bytes / 1024).toFixed(1)} KB)`); + } + } + + warningList.replaceChildren( + ...warnings.map(msg => Object.assign(document.createElement("li"), { textContent: msg })), + ); + warningsSection.hidden = warnings.length === 0; +} + +function updateDtfSize() { + if (!state.pixels) return; + const bytes = DTF.HEADER_SIZE + state.width * state.height * DTF.BPP[state.format]; + infoDtfSize.textContent = `${(bytes / 1024).toFixed(1)} KB`; + updateWarnings(); +} + +function applyImageData(width, height, data, filename, formatLabel, format) { + state.pixels = new Uint8ClampedArray(data); // defensive copy + state.width = width; + state.height = height; + state.filename = filename.replace(/\.[^/.]+$/, ""); + + // Sync format selector when loading an existing DTF + if (format !== undefined) { + state.format = format; + formatSelect.value = format; + } + + // Sidebar info + fileNameEl.textContent = filename; + infoFilename.textContent = filename; + infoSize.textContent = `${width} × ${height}`; + infoFormat.textContent = formatLabel; + infoSection.hidden = false; + updateDtfSize(); + + // Show canvas + previewEmpty.hidden = true; + previewScroll.hidden = false; + + // Enable export + btnDtf.disabled = false; + btnPng.disabled = false; + + render(); +} + +function handleFile(file) { + if (!file) return; + if (file.name.toLowerCase().endsWith(".dtf")) { + loadDTF(file); + } else { + loadStandardImage(file); + } +} + +function showError(msg) { + alert(msg); +} + +// ─── Export ─────────────────────────────────────────────────────────────────── + +function exportDTF() { + if (!state.pixels) return; + const buf = DTF.encode(state.width, state.height, state.pixels, state.format, state.redAsAlpha); + const blob = new Blob([buf], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + const a = Object.assign(document.createElement("a"), { + href: url, + download: `${state.filename}.dtf`, + }); + a.click(); + URL.revokeObjectURL(url); +} + +function exportPNG() { + if (!state.pixels) return; + DuskPNG.download(`${state.filename}.png`, state.width, state.height, state.pixels); +} + +// ─── Event listeners ───────────────────────────────────────────────────────── + +fileInput.addEventListener("change", e => handleFile(e.target.files[0])); + +scaleInput.addEventListener("input", () => { + const v = Math.max(1, Math.min(10, parseInt(scaleInput.value, 10) || 1)); + scaleInput.value = v; + state.scale = v; + render(); +}); + +bgSwatches.addEventListener("click", e => { + const btn = e.target.closest(".bg-swatch"); + if (!btn) return; + bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active")); + btn.classList.add("active"); + state.bg = btn.dataset.bg; + render(); +}); + +btnDtf.addEventListener("click", exportDTF); +btnPng.addEventListener("click", exportPNG); + +formatSelect.addEventListener("change", () => { + state.format = parseInt(formatSelect.value, 10); + redAsAlphaRow.hidden = state.format !== DTF.FORMAT_ALPHA; + updateDtfSize(); + render(); +}); + +redAsAlphaCheck.addEventListener("change", () => { + state.redAsAlpha = redAsAlphaCheck.checked; + render(); +}); + +// Drag-and-drop on the load label +function setupDrop(target) { + target.addEventListener("dragover", e => { + e.preventDefault(); + loadLabel.classList.add("drag-over"); + previewArea.classList.add("drag-over"); + }); + target.addEventListener("dragleave", () => { + loadLabel.classList.remove("drag-over"); + previewArea.classList.remove("drag-over"); + }); + target.addEventListener("drop", e => { + e.preventDefault(); + loadLabel.classList.remove("drag-over"); + previewArea.classList.remove("drag-over"); + handleFile(e.dataTransfer.files[0]); + }); +} + +setupDrop(loadLabel); +setupDrop(previewArea); diff --git a/tools/env_to_h/CMakeLists.txt b/tools/env_to_h/CMakeLists.txt deleted file mode 100644 index b97b1a2a..00000000 --- a/tools/env_to_h/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -function(dusk_env_to_h INPUT_PATH OUTPUT_NAME_RELATIVE) - set(DUSK_DEFS_TARGET_NAME "DUSK_DEFS_${OUTPUT_NAME_RELATIVE}") - dusk_run_python( - ${DUSK_DEFS_TARGET_NAME} - tools.env_to_h - --env "${CMAKE_CURRENT_LIST_DIR}/${INPUT_PATH}" - --output ${DUSK_GENERATED_HEADERS_DIR}/${OUTPUT_NAME_RELATIVE} - ) - add_dependencies(${DUSK_LIBRARY_TARGET_NAME} ${DUSK_DEFS_TARGET_NAME}) -endfunction() \ No newline at end of file diff --git a/tools/env_to_h/__main__.py b/tools/env_to_h/__main__.py deleted file mode 100644 index 88ab6a26..00000000 --- a/tools/env_to_h/__main__.py +++ /dev/null @@ -1,46 +0,0 @@ -import argparse -import os -from dotenv import load_dotenv, dotenv_values - -parser = argparse.ArgumentParser(description="Convert .env to .h defines") -parser.add_argument("--env", required=True, help="Path to .env file") -parser.add_argument("--output", required=True, help="Path to output .h file") -args = parser.parse_args() - -# Load .env file -load_dotenv(dotenv_path=args.env) -fileDefs = dotenv_values(dotenv_path=args.env) - -outHeader = "" -outHeader += "#include \"dusk.h\"\n\n" -for key, value in fileDefs.items(): - # Determine type and print out appropriate C type define. - - # Integer - try: - asInt = int(value) - outHeader += f"#define {key} {asInt}\n" - continue - except: - pass - - # Float - try: - asFloat = float(value) - outHeader += f"#define {key} {asFloat}f\n" - continue - except: - pass - - # Boolean - if value.lower() in ['true', 'false']: - asBool = '1' if value.lower() == 'true' else '0' - outHeader += f"#define {key} {asBool}\n" - continue - - # String - outHeader += f'#define {key} "{value}"\n' - -# Write to output file -with open(args.output, 'w') as outFile: - outFile.write(outHeader) \ No newline at end of file diff --git a/tools/input/csv/__main__.py b/tools/input/csv/__main__.py index f10f9d69..6055de08 100644 --- a/tools/input/csv/__main__.py +++ b/tools/input/csv/__main__.py @@ -1,60 +1,58 @@ import argparse -import os import csv +import os parser = argparse.ArgumentParser(description="Input CSV to .h defines") parser.add_argument("--csv", required=True, help="Path to Input CSV file") parser.add_argument("--output", required=True, help="Path to output .h file") args = parser.parse_args() -def csvIdToEnumName(inputId): - return "INPUT_ACTION_" + inputId.upper() +def id_enum(name): + return "INPUT_ACTION_" + name.upper() -# Load up CSV file. -outHeader = "#pragma once\n" -outHeader += '#include "dusk.h"\n\n' - -with open(args.csv, newline="", encoding="utf-8") as csvfile: - reader = csv.DictReader(csvfile) - - # CSV must have id column. +# Load CSV +input_ids = [] +with open(args.csv, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) if "id" not in reader.fieldnames: - raise Exception("CSV file must have 'id' column") - - # For each ID - inputIds = [] - inputIdValues = {} + raise ValueError("CSV must have an 'id' column") for row in reader: - inputId = row["id"] - if inputId not in inputIds: - inputIds.append(inputId) + input_id = row["id"] + if input_id not in input_ids: + input_ids.append(input_id) - # For each ID, create enum entry. - count = 0 - outHeader += "typedef enum {\n" - outHeader += f" INPUT_ACTION_NULL = 0x{count:x},\n\n" - count += 1 - for inputId in inputIds: - inputIdValues[inputId] = count - outHeader += f" {csvIdToEnumName(inputId)} = 0x{count:x},\n" - count += 1 - outHeader += f"\n INPUT_ACTION_COUNT = 0x{count:x}\n" - outHeader += "} inputaction_t;\n\n" +# Assign enum values +id_values = {input_id: i + 1 for i, input_id in enumerate(input_ids)} - # Write IDs to char array. - outHeader += f"static const char_t* INPUT_ACTION_IDS[] = {{\n" - for inputId in inputIds: - outHeader += f" [{csvIdToEnumName(inputId)}] = \"{inputId}\",\n" - outHeader += f"}};\n\n" +# Build output +out = [ + "#pragma once", + '#include "dusk.h"', + "", + "typedef enum {", + " INPUT_ACTION_NULL = 0x0,", + "", +] +for input_id in input_ids: + out.append(f" {id_enum(input_id)} = 0x{id_values[input_id]:x},") +out += [ + "", + f" INPUT_ACTION_COUNT = 0x{len(input_ids) + 1:x}", + "} inputaction_t;", + "", + "static const char_t *INPUT_ACTION_IDS[] = {", +] +for input_id in input_ids: + out.append(f" [{id_enum(input_id)}] = \"{input_id}\",") +out += [ + "};", + "", + "static const char_t *INPUT_ACTION_SCRIPT =", +] +for input_id in input_ids: + out.append(f" \"{id_enum(input_id)} = {id_values[input_id]}\\n\"") +out += [";", ""] - # Lua Script - outHeader += f"static const char_t *INPUT_ACTION_SCRIPT = \n" - for inputId in inputIds: - # Reference the enum - outHeader += f" \"{csvIdToEnumName(inputId)} = {inputIdValues[inputId]}\\n\"\n" - outHeader += f";\n\n" - -# Write to output file. os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as outFile: - outFile.write(outHeader) \ No newline at end of file +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out)) diff --git a/tools/item/csv/__main__.py b/tools/item/csv/__main__.py index f065aec9..c3c4d5fc 100644 --- a/tools/item/csv/__main__.py +++ b/tools/item/csv/__main__.py @@ -1,94 +1,104 @@ import argparse -import os import csv +import os parser = argparse.ArgumentParser(description="Item CSV to .h defines") parser.add_argument("--csv", required=True, help="Path to item CSV file") parser.add_argument("--output", required=True, help="Path to output .h file") args = parser.parse_args() -def csvIdToEnumName(itemId): - return "ITEM_ID_" + itemId.upper() +def type_enum(name): + return "ITEM_TYPE_" + name.upper() -itemIds = [] -itemTypes = [] -itemRowById = {} +def id_enum(name): + return "ITEM_ID_" + name.upper() -with open(args.csv, newline="", encoding="utf-8") as csvfile: - reader = csv.DictReader(csvfile) +# Load CSV +item_ids = [] +item_types = [] +rows = {} - # CSV must have id and type columns. +with open(args.csv, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) if "id" not in reader.fieldnames or "type" not in reader.fieldnames: - raise Exception("CSV file must have 'id' and 'type' columns") - + raise ValueError("CSV must have 'id' and 'type' columns") for row in reader: - itemId = row["id"] - itemType = row["type"] - - if itemId not in itemIds: - itemIds.append(itemId) + item_id, item_type = row["id"], row["type"] + if item_id not in item_ids: + item_ids.append(item_id) + if item_type not in item_types: + item_types.append(item_type) + rows[item_id] = row - if itemType not in itemTypes: - itemTypes.append(itemType) - - itemRowById[itemId] = row - -# Now Prep output -outHeader = "#pragma once\n" -outHeader += '#include "dusk.h"\n\n' - -itemTypeValues = {} -itemIdValues = {} +# Assign enum values — types and IDs share a single counter so values never collide count = 0 +type_values = {} +id_values = {} -# Create enum for types and ids, include null and count. -outHeader += "typedef enum {\n" -outHeader += f" ITEM_TYPE_NULL = {count},\n" -count += 1 -for itemType in itemTypes: - itemTypeValues[itemType] = count - outHeader += f" {csvIdToEnumName(itemType)} = {count},\n" +count += 1 # 0 = NULL +for t in item_types: + type_values[t] = count count += 1 -outHeader += f" ITEM_TYPE_COUNT = {count}\n" -outHeader += "} itemtype_t;\n\n" +# ITEM_TYPE_COUNT = count; item IDs continue from here +type_count = count -outHeader += "typedef enum {\n" -outHeader += f" ITEM_ID_NULL = {count},\n" -for itemId in itemIds: - itemIdValues[itemId] = count - outHeader += f" {csvIdToEnumName(itemId)} = {count},\n" +count += 1 # type_count = ITEM_ID_NULL +for i in item_ids: + id_values[i] = count count += 1 -outHeader += f" ITEM_ID_COUNT = {count}\n" -outHeader += "} itemid_t;\n\n" +id_count = count -# Create struct for item data. -outHeader += "typedef struct {\n" -outHeader += " itemid_t id;\n" -outHeader += " itemtype_t type;\n" -outHeader += " const char_t *name;\n" -outHeader += "} item_t;\n\n" +# Build output +out = [ + "#pragma once", + '#include "dusk.h"', + "", + "typedef enum {", + " ITEM_TYPE_NULL = 0,", +] +for t in item_types: + out.append(f" {type_enum(t)} = {type_values[t]},") +out += [ + f" ITEM_TYPE_COUNT = {type_count}", + "} itemtype_t;", + "", + "typedef enum {", + f" ITEM_ID_NULL = {type_count},", +] +for i in item_ids: + out.append(f" {id_enum(i)} = {id_values[i]},") +out += [ + f" ITEM_ID_COUNT = {id_count}", + "} itemid_t;", + "", + "typedef struct {", + " itemid_t id;", + " itemtype_t type;", + " const char_t *name;", + "} item_t;", + "", + "static const item_t ITEMS[] = {", +] +for i in item_ids: + row = rows[i] + out += [ + f" [{id_enum(i)}] = {{", + f" .id = {id_enum(i)},", + f" .type = {type_enum(row['type'])},", + f" .name = \"{i}\",", + " },", + ] +out += [ + "};", + "", + "static const char_t *ITEM_SCRIPT =", +] +for i in item_ids: + out.append(f" \"{id_enum(i)} = {id_values[i]}\\n\"") +for t in item_types: + out.append(f" \"{type_enum(t)} = {type_values[t]}\\n\"") +out += [";", ""] -# Create array of item data. -outHeader += f"static const item_t ITEMS[] = {{\n" -for itemId in itemIds: - outHeader += f" [{csvIdToEnumName(itemId)}] = {{\n" - outHeader += f" .id = {csvIdToEnumName(itemId)},\n" - itemType = itemRowById[itemId]["type"] - outHeader += f" .type = {csvIdToEnumName(itemType)},\n" - itemName = itemRowById[itemId]["id"] - outHeader += f" .name = \"{itemName}\",\n" - outHeader += f" }},\n" -outHeader += f"}};\n\n" - -# Create lua script defining items. -outHeader += f"static const char_t *ITEM_SCRIPT = \n" -for itemId in itemIds: - outHeader += f" \"{csvIdToEnumName(itemId)} = {itemIdValues[itemId]}\\n\"\n" -for itemType in itemTypes: - outHeader += f" \"{csvIdToEnumName(itemType)} = {itemTypeValues[itemType]}\\n\"\n" -outHeader += f";\n\n" - -# Write to output file. os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as outFile: - outFile.write(outHeader) \ No newline at end of file +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out)) diff --git a/tools/palette-indexer.html b/tools/palette-indexer.html deleted file mode 100644 index 9ae97268..00000000 --- a/tools/palette-indexer.html +++ /dev/null @@ -1,687 +0,0 @@ - - - - - - Dusk Tools / Palette Indexer - - - - - -

Dusk Palette Indexer

-

- This tool takes an input image and creates a palettized version of it. - You will get two files, a .dpf (Dusk Palette File) and a .dpt - (Dusk Palettized Texture). The first being the palette, which I recommend - reusing across multiple images, and the second being the indexed image, - which uses the colors from the palette. -

-

- Also, dusk previously supported Alpha textures, but due to so many little - platform differences I realized it is simpler and about as much work to - get palettized textures working instead. As a result I suggest creating a - single palette for your alpha textures, and reusing that globally. -

- -
-

Input Image

-
- -
-

- -
- -
-

Palette

-
- - - - -
- -
-
- -
- -
- - -
-

Palette Preview

- -

Palette

-
- -
- -

- -

Indexed Image

-
- -
-
-
- -
- -
- - - - \ No newline at end of file diff --git a/tools/run_python/CMakeLists.txt b/tools/run_python/CMakeLists.txt index efad0c44..abd8808a 100644 --- a/tools/run_python/CMakeLists.txt +++ b/tools/run_python/CMakeLists.txt @@ -1,10 +1,5 @@ # Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Copyright (c) 2026 Dominic Masters -# +# # This software is released under the MIT License. # https://opensource.org/licenses/MIT @@ -16,4 +11,4 @@ function(dusk_run_python CMAKE_TARGET_NAME PYTHON_MODULE) COMMAND ${Python3_EXECUTABLE} -m ${PYTHON_MODULE} ${ARGN} ) -endfunction() \ No newline at end of file +endfunction() diff --git a/tools/server.py b/tools/server.py new file mode 100755 index 00000000..c55f5b8f --- /dev/null +++ b/tools/server.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +import argparse +import functools +import http.server +import os + +EDITOR_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor") + +parser = argparse.ArgumentParser(description="Dusk editor tools server") +parser.add_argument("--host", default="localhost", help="Host to bind to") +parser.add_argument("--port", type=int, default=3000, help="Port to listen on") +args = parser.parse_args() + +handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=EDITOR_DIR) + +with http.server.HTTPServer((args.host, args.port), handler) as server: + print(f"Dusk editor tools: http://{args.host}:{args.port}/") + server.serve_forever() diff --git a/tools/story/csv/__main__.py b/tools/story/csv/__main__.py index e99e3b4e..f7223836 100644 --- a/tools/story/csv/__main__.py +++ b/tools/story/csv/__main__.py @@ -1,48 +1,49 @@ import argparse -import os import csv +import os parser = argparse.ArgumentParser(description="Story CSV to .h defines") parser.add_argument("--csv", required=True, help="Path to story CSV file") -parser.add_argument("--header-file", required=True, help="Path to output .h file") +parser.add_argument("--output", required=True, help="Path to output .h file") args = parser.parse_args() -def idToEnum(id): - return "STORY_FLAG_" + id.upper().replace(" ", "_") +def flag_enum(name): + return "STORY_FLAG_" + name.upper().replace(" ", "_") -# Load up CSV file. -outHeader = "#pragma once\n" -outHeader += '#include "story/storyflagdefs.h"\n\n' - -with open(args.csv, newline="", encoding="utf-8") as csvfile: - reader = csv.DictReader(csvfile) - - # CSV must have id column +# Load flags +flags = [] +with open(args.csv, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) if "id" not in reader.fieldnames: - raise Exception("CSV file must have 'id' column") - - # Generate enum - outHeader += "typedef enum {\n" - outHeader += " STORY_FLAG_NULL,\n\n" + raise ValueError("CSV must have an 'id' column") for row in reader: - id = idToEnum(row["id"].strip()) - outHeader += f" {id},\n" - outHeader += "\n STORY_FLAG_COUNT\n" - outHeader += "} storyflag_t;\n\n" + flags.append({ + "id": row["id"].strip(), + "initial": (row.get("initial") or "0").strip(), + }) - # Generate flag values - csvfile.seek(0) - reader = csv.DictReader(csvfile) +# Build output +out = [ + "#pragma once", + '#include "story/storyflagdefs.h"', + "", + "typedef enum {", + " STORY_FLAG_NULL,", + "", +] +for flag in flags: + out.append(f" {flag_enum(flag['id'])},") +out += [ + "", + " STORY_FLAG_COUNT", + "} storyflag_t;", + "", + "static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {", +] +for flag in flags: + out.append(f" [{flag_enum(flag['id'])}] = {flag['initial']},") +out += ["};", ""] - outHeader += "static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {\n" - for row in reader: - id = idToEnum(row["id"].strip()) - initial = row.get("initial", "0").strip() or "0" - outHeader += f" [{id}] = {initial},\n" - outHeader += "};\n" - -os.makedirs(os.path.dirname(args.header_file), exist_ok=True) - -# Write header -with open(args.header_file, "w", encoding="utf-8") as headerFile: - headerFile.write(outHeader) \ No newline at end of file +os.makedirs(os.path.dirname(args.output), exist_ok=True) +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out)) diff --git a/tools/texture-creator.html b/tools/texture-creator.html deleted file mode 100644 index 152f3b6f..00000000 --- a/tools/texture-creator.html +++ /dev/null @@ -1,417 +0,0 @@ - - - - - - Dusk Tools / Texture Creator - - - - - -

Dusk Texture Creator

-

- Creates texture files. This will not create palletized textures, use the - palette-indexer.html tool for that. This will instead work for all other - kinds of textures. -

- -
-
- -
-
- -
-

Settings

- -
- Texture Format: -
-
- -
- -
- -
-
-
- -
- -
-
- -
-

Preview

-
- -
-
- -
-
- -
-
- -
-
- -
-
- - - - \ No newline at end of file diff --git a/tools/tile-joiner.html b/tools/tile-joiner.html deleted file mode 100644 index b1f8a120..00000000 --- a/tools/tile-joiner.html +++ /dev/null @@ -1,235 +0,0 @@ - - - - - - Dusk Tools / Tile Joiner - - - - - -

Dusk Tile Joiner

-

- Joins multiple tiles together into a single tileset image. Optimizing and - allowing it to work on multiple platforms. Output width and height will - always be a power of two. This tool will take a while to process images, - since all images need to be loaded into memory. -

- -
-
- -
-

- -
- -
-

Settings

- -
- -
-
- -
-

Joined Preview

-
- -
- -
- - - - \ No newline at end of file diff --git a/tools/tile-slicer.html b/tools/tile-slicer.html deleted file mode 100644 index dcc62471..00000000 --- a/tools/tile-slicer.html +++ /dev/null @@ -1,391 +0,0 @@ - - - - - - Dusk Tools / Tile Slicer - - - - - -

Dusk Tile Slicer

-

- Tool allows you to take a single image and slice it into individual tiles. - This should be done as the first step of optimizing tilesets for your - dusk game. -

- -
-
- -
-

- -

Input Preview

- -
- -
-

Settings

-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
- -
-

Sliced Preview

-
-
- -
- -
-
- - - - - - \ No newline at end of file diff --git a/tools/tileset-creator.html b/tools/tileset-creator.html deleted file mode 100644 index f6715411..00000000 --- a/tools/tileset-creator.html +++ /dev/null @@ -1,546 +0,0 @@ - - - - - - Dusk Tools / Tileset Creator - - - - - -

Dusk Tileset Creator

-

- Tool to create tilesets for textures. Currently only supports well sliced - tilesets (those with fixed dimensions essentially). In the future, may - support more freeform tilesets. -

- -
-

Tileset Settings

-
- -
- -
- Define tile count by: -
- - - -
-
- -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- - -
-
- - -
-
- -
-

Preview

-
- -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - - - \ No newline at end of file