diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e39129b4..56390d44 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,7 +32,7 @@ jobs:
git \
libssl-dev
- name: Build Linux
- run: ./scripts/build-linux.sh
+ run: ./scripts/linux/build-linux.sh
- name: Upload Linux binary
uses: actions/upload-artifact@v3
with:
@@ -48,7 +48,7 @@ jobs:
- name: Setup pspdev
uses: ./.github/actions/setup-pspdev
- name: Build PSP
- run: ./scripts/build-psp.sh
+ run: ./scripts/psp/build-psp.sh
- name: Move EBOOT.PBP to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk/PSP/GAME/Dusk
@@ -103,7 +103,7 @@ jobs:
libgles2:arm64 \
libgl1-mesa-dev:arm64
- name: Build Knulli
- run: ./scripts/build-knulli.sh
+ run: ./scripts/knulli/build-knulli.sh
- name: Move output to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk
@@ -134,7 +134,7 @@ jobs:
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build GameCube
- run: ./scripts/build-gamecube.sh
+ run: ./scripts/gamecube/build-gamecube.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk
@@ -166,7 +166,7 @@ jobs:
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build GameCube ISO
- run: ./scripts/build-gamecube-iso.sh
+ run: ./scripts/gamecube/build-gamecube-iso.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk
@@ -199,7 +199,7 @@ jobs:
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build Wii
- run: ./scripts/build-wii.sh
+ run: ./scripts/wii/build-wii.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
@@ -232,7 +232,7 @@ jobs:
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build Wii ISO
- run: ./scripts/build-wii-iso.sh
+ run: ./scripts/wii/build-wii-iso.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f2cbf7af..8dea4e77 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,4 +32,4 @@ jobs:
git \
libssl-dev
- name: Run tests
- run: ./scripts/test-linux.sh
+ run: ./scripts/linux/test-linux.sh
diff --git a/editor/client/public/common/chunkterrain.js b/editor/client/public/common/chunkterrain.js
deleted file mode 100644
index 4a19360e..00000000
--- a/editor/client/public/common/chunkterrain.js
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-"use strict";
-
-// ChunkTerrain - JS port of the terrain-quad generation performed by
-// tools/asset/chunk/__main__.py (build_terrain_verts / _tile_quad /
-// _RAMP_CORNERS) so the editor's 3D preview can show the same geometry the
-// Python compiler will bake into the chunk's terrain mesh, without waiting
-// for a compile pass. Vertex layout matches DMF: interleaved [u, v, x, y, z].
-
-const ChunkTerrain = (() => {
- const CHUNK_WIDTH = 16;
- const CHUNK_HEIGHT = 16;
- const CHUNK_DEPTH = 4;
- const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH;
-
- // World-space Z distance of one Z-layer/story, in world-float space.
- // Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h.
- const WORLD_LAYER_HEIGHT = Math.SQRT1_2;
-
- const TILE_SHAPE_NULL = 0;
- const TILE_SHAPE_GROUND = 1;
- const TILE_SHAPE_RAMP_NORTH = 2;
- const TILE_SHAPE_RAMP_EAST = 3;
- const TILE_SHAPE_RAMP_SOUTH = 4;
- const TILE_SHAPE_RAMP_WEST = 5;
- const TILE_SHAPE_RAMP_NORTHEAST = 6;
- const TILE_SHAPE_RAMP_NORTHWEST = 7;
- const TILE_SHAPE_RAMP_SOUTHEAST = 8;
- const TILE_SHAPE_RAMP_SOUTHWEST = 9;
- const TILE_SHAPE_RAMP_NORTHEAST_INNER = 10;
- const TILE_SHAPE_RAMP_NORTHWEST_INNER = 11;
- const TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12;
- const TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13;
-
- // Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z.
- // NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1.
- // Diagonal outer-corner ramps: only the named corner raised, rest at 0.
- // Diagonal inner-corner ramps: only the corner opposite the named one is
- // lowered to 0, the rest (including the named corner) stay raised at 1.
- const RAMP_CORNERS = {
- [TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0],
- [TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0],
- [TILE_SHAPE_RAMP_SOUTH]: [1.0, 1.0, 0.0, 0.0],
- [TILE_SHAPE_RAMP_EAST]: [0.0, 1.0, 1.0, 0.0],
- [TILE_SHAPE_RAMP_WEST]: [1.0, 0.0, 0.0, 1.0],
- [TILE_SHAPE_RAMP_NORTHEAST]: [0.0, 0.0, 1.0, 0.0],
- [TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0],
- [TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0],
- [TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0],
- [TILE_SHAPE_RAMP_NORTHEAST_INNER]: [0.0, 1.0, 1.0, 1.0],
- [TILE_SHAPE_RAMP_NORTHWEST_INNER]: [1.0, 0.0, 1.0, 1.0],
- [TILE_SHAPE_RAMP_SOUTHEAST_INNER]: [1.0, 1.0, 1.0, 0.0],
- [TILE_SHAPE_RAMP_SOUTHWEST_INNER]: [1.0, 1.0, 0.0, 1.0],
- };
-
- function tileIndex(x, y, z) {
- return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT;
- }
-
- // Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
- // Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
- function pushTileQuad(out, u0, u1, v0, v1, fx, fy, swZ, seZ, neZ, nwZ) {
- out.push(
- u0, v0, fx, fy, swZ,
- u1, v0, fx + 1, fy, seZ,
- u1, v1, fx + 1, fy + 1, neZ,
- u0, v0, fx, fy, swZ,
- u1, v1, fx + 1, fy + 1, neZ,
- u0, v1, fx, fy + 1, nwZ,
- );
- }
-
- // Generate terrain quads for every non-null tile in a flat tiles array
- // (length TILE_COUNT, indexed via tileIndex). Returns a Float32Array of
- // interleaved [u, v, x, y, z] vertices, matching DMF.VERTEX_FLOATS.
- function buildTerrainVerts(tiles) {
- const out = [];
- for(let z = 0; z < CHUNK_DEPTH; z++) {
- for(let y = 0; y < CHUNK_HEIGHT; y++) {
- for(let x = 0; x < CHUNK_WIDTH; x++) {
- const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
- const corners = RAMP_CORNERS[type];
- if(!corners) continue;
-
- const u0 = x / CHUNK_WIDTH;
- const u1 = (x + 1) / CHUNK_WIDTH;
- const v0 = y / CHUNK_HEIGHT;
- const v1 = (y + 1) / CHUNK_HEIGHT;
- const [sw, se, ne, nw] = corners;
-
- pushTileQuad(
- out, u0, u1, v0, v1, x, y,
- (z + sw) * WORLD_LAYER_HEIGHT,
- (z + se) * WORLD_LAYER_HEIGHT,
- (z + ne) * WORLD_LAYER_HEIGHT,
- (z + nw) * WORLD_LAYER_HEIGHT
- );
- }
- }
- }
- return new Float32Array(out);
- }
-
- // Four edges (SW-SE, SE-NE, NE-NW, NW-SW) of a tile's quad as line
- // segments, lifted slightly above the terrain surface to avoid
- // z-fighting - same corner-height convention as pushTileQuad().
- function pushTileGridLines(out, fx, fy, swZ, seZ, neZ, nwZ) {
- const eps = 0.01;
- const sw = [fx, fy, swZ + eps];
- const se = [fx + 1, fy, seZ + eps];
- const ne = [fx + 1, fy + 1, neZ + eps];
- const nw = [fx, fy + 1, nwZ + eps];
- const edges = [sw, se, se, ne, ne, nw, nw, sw];
- for(const [x, y, z] of edges) out.push(0, 0, x, y, z);
- }
-
- // Generate grid-outline line segments for every non-null tile, following
- // the same per-corner heights as buildTerrainVerts() so lines hug ramps
- // instead of cutting through them. Returns a Float32Array of interleaved
- // [u, v, x, y, z] vertices meant to be drawn with gl.LINES (pairs).
- function buildGridLines(tiles) {
- const out = [];
- for(let z = 0; z < CHUNK_DEPTH; z++) {
- for(let y = 0; y < CHUNK_HEIGHT; y++) {
- for(let x = 0; x < CHUNK_WIDTH; x++) {
- const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
- const corners = RAMP_CORNERS[type];
- if(!corners) continue;
-
- const [sw, se, ne, nw] = corners;
- pushTileGridLines(
- out, x, y,
- (z + sw) * WORLD_LAYER_HEIGHT,
- (z + se) * WORLD_LAYER_HEIGHT,
- (z + ne) * WORLD_LAYER_HEIGHT,
- (z + nw) * WORLD_LAYER_HEIGHT
- );
- }
- }
- }
- return new Float32Array(out);
- }
-
- return Object.freeze({
- CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT, WORLD_LAYER_HEIGHT,
- TILE_SHAPE_NULL, TILE_SHAPE_GROUND,
- TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
- TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
- TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST,
- TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
- TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
- TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
- RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines,
- });
-})();
diff --git a/editor/client/public/common/dmf.js b/editor/client/public/common/dmf.js
deleted file mode 100644
index 2a293cb5..00000000
--- a/editor/client/public/common/dmf.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-"use strict";
-
-// DMF - Dusk Mesh Format
-//
-// Header (12 bytes):
-// [0-3] "DMF\0" magic
-// [4-7] uint32 version (little-endian)
-// [8-11] uint32 vertCount (little-endian)
-//
-// Followed by vertCount vertices, each 20 bytes:
-// [0-7] float32[2] uv (little-endian)
-// [8-19] float32[3] pos (little-endian)
-
-const DMF = (() => {
- const MAGIC = [0x44, 0x4d, 0x46, 0x00]; // "DMF\0"
- const VERSION = 1;
- const HEADER_SIZE = 12;
- const VERTEX_FLOATS = 5; // uv[2] + pos[3]
- const VERTEX_SIZE = VERTEX_FLOATS * 4;
-
- // Decode a DMF ArrayBuffer into { version, vertexCount, floats }, where
- // floats is a Float32Array of tightly interleaved [u, v, x, y, z] per
- // vertex - ready to hand straight to gl.bufferData().
- 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 DMF");
- if(
- bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] ||
- bytes[2] !== MAGIC[2] || bytes[3] !== MAGIC[3]
- ) {
- throw new Error("Invalid DMF magic bytes - not a DMF file");
- }
-
- const version = view.getUint32(4, true);
- if(version !== VERSION) {
- throw new Error(`Unsupported DMF version: ${version}`);
- }
-
- const vertexCount = view.getUint32(8, true);
- const expected = HEADER_SIZE + vertexCount * VERTEX_SIZE;
- if(bytes.length < expected) {
- throw new Error(`DMF vertex data truncated (expected ${expected} bytes, got ${bytes.length})`);
- }
-
- const floats = new Float32Array(vertexCount * VERTEX_FLOATS);
- for(let i = 0; i < floats.length; i++) {
- floats[i] = view.getFloat32(HEADER_SIZE + i * 4, true);
- }
-
- return { version, vertexCount, floats };
- }
-
- return Object.freeze({
- decode,
- VERSION, HEADER_SIZE, VERTEX_FLOATS, VERTEX_SIZE,
- });
-})();
diff --git a/editor/client/public/index.html b/editor/client/public/index.html
deleted file mode 100644
index 2eb5fe25..00000000
--- a/editor/client/public/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
Dusk Map Editor
-
Browse and edit project assets.
-
-
diff --git a/editor/client/public/map/index.html b/editor/client/public/map/index.html
deleted file mode 100644
index 5e491709..00000000
--- a/editor/client/public/map/index.html
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
-
Meshes
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Preview (scroll to zoom)
-
-
-
-
-
-
-
-
diff --git a/editor/client/public/map/map.css b/editor/client/public/map/map.css
deleted file mode 100644
index 54b8843d..00000000
--- a/editor/client/public/map/map.css
+++ /dev/null
@@ -1,65 +0,0 @@
-/* Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-.tile-palette {
- display: flex;
- flex-direction: column;
- flex-wrap: wrap;
- gap: 6px;
- max-height: 600px;
-}
-
-.tile-swatch {
- position: relative;
- width: 32px;
- height: 32px;
- border: 2px solid transparent;
- border-radius: 4px;
- cursor: pointer;
- padding: 0;
-}
-
-.tile-swatch.active {
- border-color: #fff;
-}
-
-.tile-swatch-icon {
- position: absolute;
- top: 0;
- left: 0;
- pointer-events: none;
-}
-
-.dpad {
- display: grid;
- grid-template-columns: repeat(3, 2rem);
- grid-template-rows: repeat(3, 2rem);
- grid-template-areas:
- ". n ."
- "w . e"
- ". s .";
- gap: 2px;
-}
-
-.dpad-n { grid-area: n; }
-.dpad-w { grid-area: w; }
-.dpad-e { grid-area: e; }
-.dpad-s { grid-area: s; }
-.dpad button { padding: 0; }
-
-.tile-grid {
- image-rendering: pixelated;
- cursor: crosshair;
- touch-action: none;
- max-width: 100%;
- height: auto;
-}
-
-#previewCanvas {
- width: 100%;
- aspect-ratio: 1 / 1;
- height: auto;
-}
diff --git a/editor/client/public/map/map.js b/editor/client/public/map/map.js
deleted file mode 100644
index 0d883c12..00000000
--- a/editor/client/public/map/map.js
+++ /dev/null
@@ -1,920 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-"use strict";
-
-(() => {
- const SHAPES = [
- { type: ChunkTerrain.TILE_SHAPE_NULL, name: "Erase", color: "#20232a" },
- { type: ChunkTerrain.TILE_SHAPE_GROUND, name: "Ground", color: "#4caf50" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTH, name: "Ramp North", color: "#2196f3" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_EAST, name: "Ramp East", color: "#03a9f4" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTH, name: "Ramp South", color: "#00bcd4" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_WEST, name: "Ramp West", color: "#009688" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST, name: "Ramp NE", color: "#ff9800" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, name: "Ramp NE Inner", color: "#795548" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, name: "Ramp NW Inner", color: "#607d8b" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, name: "Ramp SE Inner", color: "#8bc34a" },
- { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, name: "Ramp SW Inner", color: "#ffc107" },
- ];
- const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color]));
-
- // Arrow direction per ramp, in canvas/screen space: north is up (dy < 0),
- // east is right (dx > 0) - matches renderGrid()'s north-is-up convention.
- const SHAPE_DIRECTIONS = {
- [ChunkTerrain.TILE_SHAPE_RAMP_NORTH]: { dx: 0, dy: -1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_SOUTH]: { dx: 0, dy: 1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_EAST]: { dx: 1, dy: 0 },
- [ChunkTerrain.TILE_SHAPE_RAMP_WEST]: { dx: -1, dy: 0 },
- [ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST]: { dx: 1, dy: -1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER]: { dx: 1, dy: -1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER]: { dx: -1, dy: -1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER]: { dx: 1, dy: 1 },
- [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
- };
-
- const INNER_CORNER_SHAPES = new Set([
- ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER,
- ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER,
- ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER,
- ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER,
- ]);
-
- // Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY).
- function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) {
- ctx.strokeStyle = "#ffffff";
- ctx.fillStyle = "#ffffff";
- ctx.lineWidth = Math.max(1, size * 0.08);
- ctx.lineCap = "round";
-
- ctx.beginPath();
- ctx.moveTo(tailX, tailY);
- ctx.lineTo(tipX, tipY);
- ctx.stroke();
-
- const headLen = size * 0.18;
- const angle = Math.atan2(tipY - tailY, tipX - tailX);
- const leftAngle = angle + Math.PI * 0.8;
- const rightAngle = angle - Math.PI * 0.8;
- ctx.beginPath();
- ctx.moveTo(tipX, tipY);
- ctx.lineTo(tipX + Math.cos(leftAngle) * headLen, tipY + Math.sin(leftAngle) * headLen);
- ctx.lineTo(tipX + Math.cos(rightAngle) * headLen, tipY + Math.sin(rightAngle) * headLen);
- ctx.closePath();
- ctx.fill();
- }
-
- // Draws an arrow centered at (cx, cy) pointing toward the ramp's high
- // side, so ramp direction is visible at a glance on both the palette
- // swatches and the tile grid. Used for cardinal ramps and outer-corner
- // ramps (a single raised corner).
- function drawArrow(ctx, cx, cy, size, dx, dy) {
- const mag = Math.hypot(dx, dy) || 1;
- const ux = dx / mag, uy = dy / mag;
- const len = size * 0.32;
- drawArrowSegment(ctx, cx - ux * len, cy - uy * len, cx + ux * len, cy + uy * len, size);
- }
-
- // Draws a plain right-angle bracket - two line segments meeting at 90
- // degrees at the tile's corner, one running along each of the two edges
- // adjacent to it - so inner-corner ramps (three corners raised, one
- // dropped) read as "the adjacent sides meeting in a 90-degree corner",
- // distinct from the single diagonal arrow used for outer-corner ramps.
- function drawCornerBracket(ctx, cx, cy, size, dx, dy) {
- const cornerDist = size * 0.42;
- const armLen = size * 0.34;
- const cornerX = cx + dx * cornerDist;
- const cornerY = cy + dy * cornerDist;
-
- ctx.strokeStyle = "#ffffff";
- ctx.lineWidth = Math.max(1, size * 0.08);
- ctx.lineCap = "round";
- ctx.lineJoin = "round";
-
- ctx.beginPath();
- ctx.moveTo(cornerX - dx * armLen, cornerY);
- ctx.lineTo(cornerX, cornerY);
- ctx.lineTo(cornerX, cornerY - dy * armLen);
- ctx.stroke();
- }
-
- // Draws the direction icon for a ramp tile `type` in a `size`x`size` area
- // centered at (cx, cy): a diagonal arrow for cardinal/outer-corner ramps,
- // or a right-angle bracket for inner-corner ramps. No-op for shapes with
- // no direction (ground, erase).
- function drawShapeIcon(ctx, cx, cy, size, type) {
- const dir = SHAPE_DIRECTIONS[type];
- if(!dir) return;
- if(INNER_CORNER_SHAPES.has(type)) {
- drawCornerBracket(ctx, cx, cy, size, dir.dx, dir.dy);
- } else {
- drawArrow(ctx, cx, cy, size, dir.dx, dir.dy);
- }
- }
-
- // Draws a small pencil icon centered in a `size`x`size` canvas.
- function drawPencilIcon(ctx, size) {
- ctx.save();
- ctx.translate(size / 2, size / 2);
- ctx.rotate(-Math.PI / 4);
-
- const shaftW = size * 0.18;
- const shaftH = size * 0.62;
-
- ctx.fillStyle = "#e0e0e0";
- ctx.fillRect(-shaftW / 2, shaftH * 0.28, shaftW, shaftH * 0.22);
-
- ctx.fillStyle = "#f0c419";
- ctx.fillRect(-shaftW / 2, -shaftH * 0.5, shaftW, shaftH * 0.78);
-
- ctx.fillStyle = "#8d6e63";
- ctx.beginPath();
- ctx.moveTo(-shaftW / 2, -shaftH * 0.5);
- ctx.lineTo(shaftW / 2, -shaftH * 0.5);
- ctx.lineTo(0, -shaftH * 0.5 - shaftW);
- ctx.closePath();
- ctx.fill();
-
- ctx.restore();
- }
-
- // Draws a small paint bucket icon centered in a `size`x`size` canvas.
- function drawBucketIcon(ctx, size) {
- ctx.save();
- ctx.translate(size / 2, size / 2);
-
- const w = size * 0.5, h = size * 0.34;
-
- ctx.strokeStyle = "#e0e0e0";
- ctx.lineWidth = Math.max(1, size * 0.06);
- ctx.beginPath();
- ctx.arc(0, -h * 0.4, w * 0.4, Math.PI, 0);
- ctx.stroke();
-
- ctx.fillStyle = "#03a9f4";
- ctx.beginPath();
- ctx.moveTo(-w / 2, -h * 0.2);
- ctx.lineTo(w / 2, -h * 0.2);
- ctx.lineTo(w * 0.32, h * 0.6);
- ctx.lineTo(-w * 0.32, h * 0.6);
- ctx.closePath();
- ctx.fill();
-
- ctx.beginPath();
- ctx.arc(-w * 0.55, h * 0.75, size * 0.06, 0, Math.PI * 2);
- ctx.fill();
-
- ctx.restore();
- }
-
- const TOOLS = [
- { id: "pencil", name: "Pencil", draw: drawPencilIcon },
- { id: "bucket", name: "Paint Bucket", draw: drawBucketIcon },
- ];
-
- let coord = { x: 0, y: 0, z: 0 };
- let tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
- let meshes = [];
- let chunkExists = false;
- let currentZLevel = 0;
- let selectedShape = ChunkTerrain.TILE_SHAPE_GROUND;
- let zoomWorldH = 20;
- let dirty = false;
- let hoverTile = null;
- let activeTool = "pencil";
-
- let mapRenderer = null;
- let terrainMesh = null;
- let gridLinesMesh = null;
- let terrainTexturePromise = null;
- let modelIndex = null;
- let neighborChunks = [];
- const modelCache = new Map();
-
- const UNDO_LIMIT = 100;
- let undoStack = [];
-
- function markDirty() {
- dirty = true;
- }
-
- function snapshotState() {
- return {
- tiles: tiles.slice(),
- meshes: meshes.map((m) => ({ file: m.file, pos: m.pos.slice() })),
- };
- }
-
- function pushUndo() {
- undoStack.push(snapshotState());
- if(undoStack.length > UNDO_LIMIT) undoStack.shift();
- }
-
- function undo() {
- if(!undoStack.length) return;
- const snap = undoStack.pop();
- tiles = snap.tiles;
- meshes = snap.meshes;
- dirty = true;
- renderGrid();
- renderMeshList();
- renderPreview();
- updateStatus("Undid last change.");
- }
-
- // Returns true if it's OK to proceed (no unsaved changes, or the user
- // confirmed discarding them).
- function confirmDiscardIfDirty() {
- if(!dirty) return true;
- return window.confirm("You have unsaved changes to this chunk. Discard them?");
- }
-
- function statusLine() {
- return document.getElementById("statusLine");
- }
-
- function updateStatus(msg, isError) {
- const el = statusLine();
- el.textContent = msg;
- el.classList.toggle("text-danger", !!isError);
- el.classList.toggle("text-muted", !isError);
- }
-
- async function apiReadJson(path) {
- const res = await fetch(`/api/read?path=${encodeURIComponent(path)}`);
- const data = await res.json();
- return { ok: res.ok, data };
- }
-
- async function apiWrite(path, content) {
- return fetch(`/api/write?path=${encodeURIComponent(path)}`, {
- method: "POST",
- body: JSON.stringify({ content }),
- });
- }
-
- async function apiDelete(path) {
- return fetch(`/api/delete?path=${encodeURIComponent(path)}`, { method: "POST" });
- }
-
- async function apiCompileChunk(x, y, z) {
- const res = await fetch(`/api/compile-chunk?x=${x}&y=${y}&z=${z}`, { method: "POST" });
- return res.json();
- }
-
- async function apiLs(path) {
- const res = await fetch(`/api/ls?path=${encodeURIComponent(path)}`);
- return res.json();
- }
-
- async function apiFind(path, ext) {
- const res = await fetch(`/api/find?path=${encodeURIComponent(path)}&ext=${encodeURIComponent(ext)}`);
- return res.json();
- }
-
- function base64ToBuffer(base64) {
- const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
- return bytes.buffer;
- }
-
- async function loadImageTexture(assetPath) {
- const { ok, data } = await apiReadJson(assetPath);
- if(!ok || data.error) return null;
- const blob = new Blob([base64ToBuffer(data.content)], { type: "image/png" });
- const bitmap = await createImageBitmap(blob);
- return mapRenderer.createTextureFromImage(bitmap);
- }
-
- async function readDmfVertices(assetPath) {
- const { ok, data } = await apiReadJson(assetPath);
- if(!ok || data.error) throw new Error(`Failed to read ${assetPath}`);
- return DMF.decode(base64ToBuffer(data.content)).floats;
- }
-
- async function ensureModelIndex() {
- if(modelIndex) return modelIndex;
- const res = await apiFind("assets/models", ".json");
- modelIndex = new Map();
- for(const file of res.files || []) {
- const base = file.split("/").pop().replace(/\.json$/, "");
- modelIndex.set(base, file);
- }
- return modelIndex;
- }
-
- // Scales a mesh's stored [x, y, z] offset for preview, matching the
- // Z scaling mapChunkLoaded() applies at runtime (chunk.meshOffsets[m][2]
- // * WORLD_LAYER_HEIGHT) - x/y stay 1:1 since only Z is height-scaled.
- function scaledMeshOffset(pos) {
- return [pos[0], pos[1], pos[2] * ChunkTerrain.WORLD_LAYER_HEIGHT];
- }
-
- // Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to
- // its model JSON by basename, mirroring find_model() in
- // tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
- // filename, not a path.
- async function resolveModelForMeshFile(file) {
- await ensureModelIndex();
- const base = file.replace(/\.[^.]+$/, "");
- const rel = modelIndex.get(base);
- if(!rel) return null;
-
- const modelPath = `assets/models/${rel}`;
- if(modelCache.has(modelPath)) return modelCache.get(modelPath);
-
- const promise = (async () => {
- const { ok, data } = await apiReadJson(modelPath);
- if(!ok || data.error) throw new Error(`Failed to read model ${modelPath}`);
- const modelDef = JSON.parse(data.content);
- const meshFloats = await readDmfVertices(`assets/${modelDef.mesh}`);
- const mesh = mapRenderer.createMesh(meshFloats);
- const texture = modelDef.texture ? await loadImageTexture(`assets/${modelDef.texture}`) : null;
- const colorBytes = modelDef.color || [255, 255, 255, 255];
- const color = colorBytes.map((c) => c / 255);
- return { mesh, texture, color };
- })();
- modelCache.set(modelPath, promise);
- return promise;
- }
-
- const MAP_NAME = "overworld";
-
- function chunkPaths(x, y, z) {
- return {
- raw: `assetsraw/maps/${MAP_NAME}/chunks/chunk_${x}_${y}_${z}.json`,
- dcf: `assets/maps/${MAP_NAME}/chunks/${x}_${y}_${z}.dcf`,
- terrainDmf: `assets/maps/${MAP_NAME}/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
- terrainModel: `assets/maps/${MAP_NAME}/models/chunks/chunk_${x}_${y}_${z}_0.json`,
- };
- }
-
- // Loads the 8 same-Z neighbors around `coord` (read-only, for context in
- // the 3D preview) and pre-builds their render data so renderPreview()
- // doesn't have to rebuild GL buffers on every paint stroke.
- async function loadNeighborChunks() {
- const offsets = [];
- for(let dy = -1; dy <= 1; dy++) {
- for(let dx = -1; dx <= 1; dx++) {
- if(dx || dy) offsets.push({ dx, dy });
- }
- }
-
- const results = await Promise.all(offsets.map(async ({ dx, dy }) => {
- const nx = coord.x + dx, ny = coord.y + dy, nz = coord.z;
- const { ok, data } = await apiReadJson(chunkPaths(nx, ny, nz).raw);
- if(!ok || data.error) return null;
-
- const json = JSON.parse(data.content);
- const neighborTiles = new Int32Array(ChunkTerrain.TILE_COUNT);
- for(const t of json.tiles || []) {
- const [tx, ty, tz] = t.pos;
- neighborTiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
- }
- const neighborMeshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
-
- const floats = ChunkTerrain.buildTerrainVerts(neighborTiles);
- const terrain = floats.length ? { mesh: mapRenderer.createMesh(floats) } : null;
-
- const models = [];
- for(const m of neighborMeshes) {
- try {
- const resolved = await resolveModelForMeshFile(m.file);
- if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
- } catch(e) {
- console.warn("Failed to load neighbor mesh preview", m.file, e);
- }
- }
-
- return {
- offset: [dx * ChunkTerrain.CHUNK_WIDTH, dy * ChunkTerrain.CHUNK_HEIGHT, 0],
- terrain,
- models,
- };
- }));
-
- neighborChunks = results.filter(Boolean);
- }
-
- async function loadChunk(x, y, z) {
- coord = { x, y, z };
- tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
- meshes = [];
- chunkExists = false;
-
- const { ok, data } = await apiReadJson(chunkPaths(x, y, z).raw);
- if(ok && !data.error) {
- chunkExists = true;
- const json = JSON.parse(data.content);
- for(const t of json.tiles || []) {
- const [tx, ty, tz] = t.pos;
- tiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
- }
- meshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
- }
-
- await loadNeighborChunks();
- dirty = false;
- undoStack = [];
-
- renderGrid();
- renderMeshList();
- renderPreview();
- updateStatus(chunkExists
- ? `Loaded chunk_${x}_${y}_${z}.json`
- : `New (empty) chunk at ${x}, ${y}, ${z} - paint a tile and save to create it.`);
- }
-
- async function saveChunk() {
- const tilesArr = [];
- for(let z = 0; z < ChunkTerrain.CHUNK_DEPTH; z++) {
- for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
- for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
- const type = tiles[ChunkTerrain.tileIndex(x, y, z)];
- if(type && type !== ChunkTerrain.TILE_SHAPE_NULL) {
- tilesArr.push({ pos: [x, y, z], type, tile: 0 });
- }
- }
- }
- }
-
- const paths = chunkPaths(coord.x, coord.y, coord.z);
-
- if(tilesArr.length === 0 && meshes.length === 0) {
- await Promise.all(Object.values(paths).map((p) => apiDelete(p)));
- chunkExists = false;
- dirty = false;
- updateStatus(`Chunk ${coord.x}, ${coord.y}, ${coord.z} is empty - deleted.`);
- return;
- }
-
- const json = { tiles: tilesArr, meshes: meshes.map((m) => ({ file: m.file, pos: m.pos })) };
- await apiWrite(paths.raw, JSON.stringify(json, null, 2));
- chunkExists = true;
- dirty = false;
- updateStatus("Saved. Compiling...");
-
- const result = await apiCompileChunk(coord.x, coord.y, coord.z);
- if(result.ok) {
- updateStatus(`Compiled OK.\n${result.stdout}`);
- } else {
- updateStatus(`Compile FAILED (code ${result.returncode}):\n${result.stderr}`, true);
- }
- }
-
- function buildPalette() {
- const el = document.getElementById("tilePalette");
- el.innerHTML = "";
- for(const s of SHAPES) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "tile-swatch" + (s.type === selectedShape ? " active" : "");
- btn.style.background = s.color;
- btn.title = s.name;
- btn.addEventListener("click", () => {
- selectedShape = s.type;
- buildPalette();
- });
-
- if(SHAPE_DIRECTIONS[s.type]) {
- const icon = document.createElement("canvas");
- icon.width = 32;
- icon.height = 32;
- icon.className = "tile-swatch-icon";
- drawShapeIcon(icon.getContext("2d"), 16, 16, 32, s.type);
- btn.appendChild(icon);
- }
-
- el.appendChild(btn);
- }
- }
-
- function buildToolPalette() {
- const el = document.getElementById("toolPalette");
- el.innerHTML = "";
- for(const tool of TOOLS) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "tile-swatch" + (tool.id === activeTool ? " active" : "");
- btn.style.background = "#2a2d33";
- btn.title = tool.name;
- btn.addEventListener("click", () => {
- activeTool = tool.id;
- buildToolPalette();
- });
-
- const icon = document.createElement("canvas");
- icon.width = 32;
- icon.height = 32;
- icon.className = "tile-swatch-icon";
- tool.draw(icon.getContext("2d"), 32);
- btn.appendChild(icon);
-
- el.appendChild(btn);
- }
- }
-
- function canvasToTile(e, canvas) {
- const rect = canvas.getBoundingClientRect();
- const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
- const px = (e.clientX - rect.left) * (canvas.width / rect.width);
- const py = (e.clientY - rect.top) * (canvas.height / rect.height);
- const x = Math.floor(px / cell);
- const yFromTop = Math.floor(py / cell);
- const y = ChunkTerrain.CHUNK_HEIGHT - 1 - yFromTop;
- if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) return null;
- return { x, y };
- }
-
- function renderGrid() {
- const canvas = document.getElementById("gridCanvas");
- const ctx = canvas.getContext("2d");
- const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
- for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
- const type = tiles[ChunkTerrain.tileIndex(x, y, currentZLevel)];
- const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
- ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
- ctx.fillRect(x * cell, sy, cell, cell);
- ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
- ctx.strokeRect(x * cell, sy, cell, cell);
-
- drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type);
- }
- }
-
- ctx.fillStyle = "#ffffff";
- for(const m of meshes) {
- const [mx, my, mz] = m.pos;
- if(Math.round(mz) !== currentZLevel) continue;
- const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell;
- ctx.beginPath();
- ctx.arc(mx * cell + cell / 2, sy + cell / 2, cell * 0.3, 0, Math.PI * 2);
- ctx.fill();
- }
-
- if(hoverTile) {
- const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - hoverTile.y) * cell;
- ctx.strokeStyle = "#ffeb3b";
- ctx.lineWidth = 2;
- ctx.strokeRect(hoverTile.x * cell + 1, sy + 1, cell - 2, cell - 2);
- }
- }
-
- function hitTestMesh(e, canvas) {
- const rect = canvas.getBoundingClientRect();
- const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
- const px = (e.clientX - rect.left) * (canvas.width / rect.width);
- const py = (e.clientY - rect.top) * (canvas.height / rect.height);
-
- let closest = -1;
- let closestDist = cell * 0.5;
- meshes.forEach((m, i) => {
- const [mx, my, mz] = m.pos;
- if(Math.round(mz) !== currentZLevel) return;
- const cx = mx * cell + cell / 2;
- const cy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell + cell / 2;
- const dist = Math.hypot(px - cx, py - cy);
- if(dist < closestDist) { closestDist = dist; closest = i; }
- });
- return closest;
- }
-
- function moveMeshTo(e, index) {
- const canvas = document.getElementById("gridCanvas");
- const t = canvasToTile(e, canvas);
- if(!t) return;
- meshes[index].pos[0] = t.x;
- meshes[index].pos[1] = t.y;
- markDirty();
- renderGrid();
- renderMeshList();
- renderPreview();
- }
-
- function paintAt(e) {
- const canvas = document.getElementById("gridCanvas");
- const t = canvasToTile(e, canvas);
- if(!t) return;
- tiles[ChunkTerrain.tileIndex(t.x, t.y, currentZLevel)] = selectedShape;
- markDirty();
- renderGrid();
- renderPreview();
- }
-
- // 4-connected flood fill of same-type tiles at the current Z-level,
- // starting from (startX, startY), replacing them with selectedShape.
- function floodFill(startX, startY) {
- const targetType = tiles[ChunkTerrain.tileIndex(startX, startY, currentZLevel)];
- if(targetType === selectedShape) return;
-
- const stack = [[startX, startY]];
- const visited = new Set();
- while(stack.length) {
- const [x, y] = stack.pop();
- const key = `${x},${y}`;
- if(visited.has(key)) continue;
- if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) continue;
- visited.add(key);
-
- const idx = ChunkTerrain.tileIndex(x, y, currentZLevel);
- if(tiles[idx] !== targetType) continue;
- tiles[idx] = selectedShape;
- stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
- }
- }
-
- function fillAt(e) {
- const canvas = document.getElementById("gridCanvas");
- const t = canvasToTile(e, canvas);
- if(!t) return;
- pushUndo();
- floodFill(t.x, t.y);
- markDirty();
- renderGrid();
- renderPreview();
- }
-
- function rebuildTerrainMesh() {
- const floats = ChunkTerrain.buildTerrainVerts(tiles);
- terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null;
-
- const lineFloats = ChunkTerrain.buildGridLines(tiles);
- gridLinesMesh = lineFloats.length ? mapRenderer.createMesh(lineFloats) : null;
- }
-
- // Keeps the preview canvas's backing-store resolution matched to its
- // actual displayed CSS size (times devicePixelRatio) - the canvas
- // otherwise stays at its fixed width/height attributes (512x512) while
- // CSS stretches it to fill the column, which upscales and blurs the
- // WebGL output on wider or high-DPI screens.
- function resizePreviewCanvas() {
- const canvas = document.getElementById("previewCanvas");
- const dpr = window.devicePixelRatio || 1;
- const displayWidth = Math.max(1, Math.round(canvas.clientWidth * dpr));
- const displayHeight = Math.max(1, Math.round(canvas.clientHeight * dpr));
- if(canvas.width !== displayWidth || canvas.height !== displayHeight) {
- canvas.width = displayWidth;
- canvas.height = displayHeight;
- }
- }
-
- async function renderPreview() {
- if(!mapRenderer) return;
- resizePreviewCanvas();
- rebuildTerrainMesh();
-
- const models = [];
- for(const m of meshes) {
- try {
- const resolved = await resolveModelForMeshFile(m.file);
- if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
- } catch(e) {
- console.warn("Failed to load mesh preview", m.file, e);
- }
- }
-
- let terrainTexture = null;
- if(terrainMesh || neighborChunks.some((n) => n.terrain)) {
- if(!terrainTexturePromise) terrainTexturePromise = loadImageTexture("assets/tiles.png");
- terrainTexture = await terrainTexturePromise;
- }
-
- const neighbors = neighborChunks.map((n) => ({
- offset: n.offset,
- terrain: n.terrain ? { mesh: n.terrain.mesh, texture: terrainTexture } : null,
- models: n.models,
- }));
-
- const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT;
- mapRenderer.render({
- target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight],
- worldH: zoomWorldH,
- terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
- gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null,
- models,
- neighbors,
- highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null,
- });
- }
-
- function renderMeshList() {
- const ul = document.getElementById("meshList");
- ul.innerHTML = "";
- meshes.forEach((m, i) => {
- const li = document.createElement("li");
- li.className = "list-group-item d-flex align-items-center gap-2 flex-wrap";
-
- const label = document.createElement("span");
- label.className = "me-auto";
- label.textContent = m.file;
- li.appendChild(label);
-
- ["X", "Y", "Z"].forEach((axis, axisIndex) => {
- const input = document.createElement("input");
- input.type = "number";
- input.className = "form-control form-control-sm";
- input.style.width = "5rem";
- input.title = axis;
- input.value = m.pos[axisIndex];
- input.addEventListener("change", () => {
- pushUndo();
- m.pos[axisIndex] = parseFloat(input.value) || 0;
- markDirty();
- renderGrid();
- renderPreview();
- });
- li.appendChild(input);
- });
-
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "btn btn-sm btn-outline-danger";
- btn.textContent = "Remove";
- btn.addEventListener("click", () => {
- pushUndo();
- meshes.splice(i, 1);
- markDirty();
- renderMeshList();
- renderGrid();
- renderPreview();
- });
-
- li.appendChild(btn);
- ul.appendChild(li);
- });
- }
-
- async function buildMeshPicker() {
- await ensureModelIndex();
- const select = document.getElementById("meshPicker");
- select.innerHTML = "";
- for(const [base, rel] of [...modelIndex.entries()].sort()) {
- const opt = document.createElement("option");
- opt.value = base;
- opt.textContent = rel;
- select.appendChild(opt);
- }
- }
-
- async function buildChunkBrowseList() {
- const list = document.getElementById("chunkBrowseList");
- list.innerHTML = "";
- const res = await apiLs(`assetsraw/maps/${MAP_NAME}/chunks`);
- for(const entry of res.entries || []) {
- const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/);
- if(!m) continue;
- const [, x, y, z] = m;
-
- const li = document.createElement("li");
- const a = document.createElement("a");
- a.className = "dropdown-item";
- a.href = "#";
- a.textContent = `${x}, ${y}, ${z}`;
- a.addEventListener("click", (e) => {
- e.preventDefault();
- goToChunk(parseInt(x, 10), parseInt(y, 10), parseInt(z, 10));
- });
-
- li.appendChild(a);
- list.appendChild(li);
- }
- }
-
- function setCoordInputs(x, y, z) {
- document.getElementById("coordX").value = x;
- document.getElementById("coordY").value = y;
- document.getElementById("coordZ").value = z;
- }
-
- function goToChunk(x, y, z) {
- if(!confirmDiscardIfDirty()) return;
- setCoordInputs(x, y, z);
- loadChunk(x, y, z);
- }
-
- function bindEvents() {
- const gridCanvas = document.getElementById("gridCanvas");
- let painting = false;
- let draggingMeshIndex = -1;
-
- gridCanvas.addEventListener("mousedown", (e) => {
- if(activeTool === "bucket") {
- fillAt(e);
- return;
- }
-
- const hit = hitTestMesh(e, gridCanvas);
- pushUndo();
- if(hit !== -1) {
- draggingMeshIndex = hit;
- return;
- }
- painting = true;
- paintAt(e);
- });
- window.addEventListener("mouseup", () => { painting = false; draggingMeshIndex = -1; });
- gridCanvas.addEventListener("mousemove", (e) => {
- hoverTile = canvasToTile(e, gridCanvas);
- if(draggingMeshIndex !== -1) { moveMeshTo(e, draggingMeshIndex); return; }
- if(painting) { paintAt(e); return; }
- renderGrid();
- renderPreview();
- });
- gridCanvas.addEventListener("mouseleave", () => {
- hoverTile = null;
- renderGrid();
- renderPreview();
- });
-
- function setZLevel(z) {
- currentZLevel = Math.max(0, Math.min(ChunkTerrain.CHUNK_DEPTH - 1, z));
- document.getElementById("zLevel").value = currentZLevel;
- document.getElementById("zLevelLabel").textContent = currentZLevel;
- renderGrid();
- renderPreview();
- }
-
- document.getElementById("zLevel").addEventListener("input", (e) => {
- setZLevel(parseInt(e.target.value, 10));
- });
- document.getElementById("zLevelDown").addEventListener("click", () => setZLevel(currentZLevel - 1));
- document.getElementById("zLevelUp").addEventListener("click", () => setZLevel(currentZLevel + 1));
-
- document.getElementById("btnLoad").addEventListener("click", () => {
- const x = parseInt(document.getElementById("coordX").value, 10) || 0;
- const y = parseInt(document.getElementById("coordY").value, 10) || 0;
- const z = parseInt(document.getElementById("coordZ").value, 10) || 0;
- goToChunk(x, y, z);
- });
-
- // NORTH=+Y, EAST=+X (matches tile-shape convention used by the tile
- // grid and tools/asset/chunk/__main__.py).
- document.getElementById("navN").addEventListener("click", () => goToChunk(coord.x, coord.y + 1, coord.z));
- document.getElementById("navS").addEventListener("click", () => goToChunk(coord.x, coord.y - 1, coord.z));
- document.getElementById("navE").addEventListener("click", () => goToChunk(coord.x + 1, coord.y, coord.z));
- document.getElementById("navW").addEventListener("click", () => goToChunk(coord.x - 1, coord.y, coord.z));
- document.getElementById("navUp").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z + 1));
- document.getElementById("navDown").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z - 1));
-
- function runSave() {
- saveChunk().catch((e) => updateStatus(`Save failed: ${e.message}`, true));
- }
-
- document.getElementById("btnSave").addEventListener("click", runSave);
-
- document.getElementById("btnAddMesh").addEventListener("click", () => {
- const base = document.getElementById("meshPicker").value;
- if(!base) return;
- const pos = [
- parseFloat(document.getElementById("meshX").value) || 0,
- parseFloat(document.getElementById("meshY").value) || 0,
- parseFloat(document.getElementById("meshZ").value) || 0,
- ];
- pushUndo();
- meshes.push({ file: `${base}.dmf`, pos });
- markDirty();
- renderMeshList();
- renderGrid();
- renderPreview();
- });
-
- document.getElementById("previewCanvas").addEventListener("wheel", (e) => {
- e.preventDefault();
- zoomWorldH = Math.min(60, Math.max(4, zoomWorldH + Math.sign(e.deltaY)));
- renderPreview();
- }, { passive: false });
-
- window.addEventListener("resize", () => renderPreview());
-
- window.addEventListener("keydown", (e) => {
- if(!(e.ctrlKey || e.metaKey)) return;
- const key = e.key.toLowerCase();
- if(key === "z") {
- e.preventDefault();
- undo();
- } else if(key === "s") {
- e.preventDefault();
- runSave();
- }
- });
- }
-
- document.addEventListener("DOMContentLoaded", async () => {
- buildPalette();
- buildToolPalette();
- mapRenderer = MapRenderer.create(document.getElementById("previewCanvas"));
- bindEvents();
- await Promise.all([buildChunkBrowseList(), buildMeshPicker()]);
- await loadChunk(coord.x, coord.y, coord.z);
- });
-})();
diff --git a/editor/client/public/map/renderer.js b/editor/client/public/map/renderer.js
deleted file mode 100644
index fc9c7470..00000000
--- a/editor/client/public/map/renderer.js
+++ /dev/null
@@ -1,298 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-"use strict";
-
-// MapRenderer - a minimal, dependency-free WebGL renderer for the chunk
-// preview pane. The camera formula is ported 1:1 from
-// src/dusk/scene/overworld/sceneoverworld.c (lines 62-81) so the preview
-// approximates the game's own fixed-angle 3rd-person camera: the eye sits
-// worldH units "behind" the target along Y and a proportional distance
-// "above" along Z, always looking at the target, up = (0, 1, 0).
-//
-// The material model mirrors SHADER_UNLIT: a texture sampler multiplied by
-// a flat RGBA tint, with a 1x1 white fallback texture standing in for
-// untextured models.
-
-const MapRenderer = (() => {
- const VERT_SRC = `
- attribute vec2 aUV;
- attribute vec3 aPos;
- uniform mat4 uModel;
- uniform mat4 uView;
- uniform mat4 uProjection;
- varying vec2 vUV;
- void main() {
- vUV = aUV;
- gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
- }
- `;
-
- const FRAG_SRC = `
- precision mediump float;
- varying vec2 vUV;
- uniform sampler2D uTexture;
- uniform vec4 uColor;
- void main() {
- gl_FragColor = texture2D(uTexture, vUV) * uColor;
- }
- `;
-
- function mat4Identity(out) {
- out.fill(0);
- out[0] = out[5] = out[10] = out[15] = 1;
- return out;
- }
-
- function mat4Translation(out, v) {
- mat4Identity(out);
- out[12] = v[0];
- out[13] = v[1];
- out[14] = v[2];
- return out;
- }
-
- function mat4Perspective(out, fovy, aspect, near, far) {
- const f = 1.0 / Math.tan(fovy / 2);
- out.fill(0);
- out[0] = f / aspect;
- out[5] = f;
- out[10] = (far + near) / (near - far);
- out[11] = -1;
- out[14] = (2 * far * near) / (near - far);
- return out;
- }
-
- function vec3Sub(a, b) {
- return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
- }
-
- function vec3Cross(a, b) {
- return [
- a[1] * b[2] - a[2] * b[1],
- a[2] * b[0] - a[0] * b[2],
- a[0] * b[1] - a[1] * b[0],
- ];
- }
-
- function vec3Normalize(a) {
- const len = Math.hypot(a[0], a[1], a[2]) || 1;
- return [a[0] / len, a[1] / len, a[2] / len];
- }
-
- function mat4LookAt(out, eye, center, up) {
- const zAxis = vec3Normalize(vec3Sub(eye, center));
- const xAxis = vec3Normalize(vec3Cross(up, zAxis));
- const yAxis = vec3Cross(zAxis, xAxis);
-
- out[0] = xAxis[0]; out[1] = yAxis[0]; out[2] = zAxis[0]; out[3] = 0;
- out[4] = xAxis[1]; out[5] = yAxis[1]; out[6] = zAxis[1]; out[7] = 0;
- out[8] = xAxis[2]; out[9] = yAxis[2]; out[10] = zAxis[2]; out[11] = 0;
- out[12] = -(xAxis[0] * eye[0] + xAxis[1] * eye[1] + xAxis[2] * eye[2]);
- out[13] = -(yAxis[0] * eye[0] + yAxis[1] * eye[1] + yAxis[2] * eye[2]);
- out[14] = -(zAxis[0] * eye[0] + zAxis[1] * eye[1] + zAxis[2] * eye[2]);
- out[15] = 1;
- return out;
- }
-
- // Ported from sceneoverworld.c:62-81. Returns { eye, view }.
- function computeCamera(target, worldH) {
- const fov = Math.PI / 4; // glm_rad(45.0f)
- const zDist = (worldH * 0.5) / Math.tan(fov * 0.5);
- const offset = -worldH;
-
- const eye = [target[0], target[1] + offset, target[2] + zDist];
- const view = mat4LookAt(new Float32Array(16), eye, target, [0, 1, 0]);
- return { eye, view, fov };
- }
-
- function compileShader(gl, type, src) {
- const shader = gl.createShader(type);
- gl.shaderSource(shader, src);
- gl.compileShader(shader);
- if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
- const info = gl.getShaderInfoLog(shader);
- gl.deleteShader(shader);
- throw new Error(`Shader compile failed: ${info}`);
- }
- return shader;
- }
-
- function create(canvas) {
- const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
- if(!gl) throw new Error("WebGL is not available in this browser");
-
- const program = gl.createProgram();
- gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, VERT_SRC));
- gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC));
- gl.linkProgram(program);
- if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
- throw new Error(`Program link failed: ${gl.getProgramInfoLog(program)}`);
- }
-
- const attribs = {
- aUV: gl.getAttribLocation(program, "aUV"),
- aPos: gl.getAttribLocation(program, "aPos"),
- };
- const uniforms = {
- uModel: gl.getUniformLocation(program, "uModel"),
- uView: gl.getUniformLocation(program, "uView"),
- uProjection: gl.getUniformLocation(program, "uProjection"),
- uTexture: gl.getUniformLocation(program, "uTexture"),
- uColor: gl.getUniformLocation(program, "uColor"),
- };
-
- const whiteTexture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, whiteTexture);
- gl.texImage2D(
- gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
- new Uint8Array([255, 255, 255, 255]),
- );
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
-
- gl.enable(gl.DEPTH_TEST);
- gl.enable(gl.BLEND);
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
- // Terrain/mesh winding isn't guaranteed consistent relative to this
- // preview's fixed camera angle, so backface culling stays off - this
- // is an authoring preview, not a performance-sensitive renderer, and
- // an invisible mesh is a worse failure mode than a visible backface.
-
- // Uploads interleaved [u, v, x, y, z] float data (DMF/terrain layout)
- // into a GL buffer. Returns { buffer, vertexCount }.
- function createMesh(floats) {
- const buffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
- gl.bufferData(gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW);
- return { buffer, vertexCount: floats.length / 5 };
- }
-
- function createTextureFromImage(image) {
- const texture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, texture);
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- return texture;
- }
-
- function drawMesh(mesh, model, texture, color, depthWrite, mode) {
- gl.depthMask(depthWrite !== false);
- gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer);
- gl.enableVertexAttribArray(attribs.aUV);
- gl.vertexAttribPointer(attribs.aUV, 2, gl.FLOAT, false, 20, 0);
- gl.enableVertexAttribArray(attribs.aPos);
- gl.vertexAttribPointer(attribs.aPos, 3, gl.FLOAT, false, 20, 8);
-
- gl.uniformMatrix4fv(uniforms.uModel, false, model);
- gl.uniform4f(uniforms.uColor, color[0], color[1], color[2], color[3]);
-
- gl.activeTexture(gl.TEXTURE0);
- gl.bindTexture(gl.TEXTURE_2D, texture || whiteTexture);
- gl.uniform1i(uniforms.uTexture, 0);
-
- gl.drawArrays(mode || gl.TRIANGLES, 0, mesh.vertexCount);
- }
-
- // Builds a 4-vertex outline of the tile at (x, y, z), lifted slightly
- // to avoid z-fighting with the terrain surface beneath it.
- function buildHighlightMesh(x, y, z) {
- const eps = 0.02;
- return createMesh(new Float32Array([
- 0, 0, x, y, z + eps,
- 0, 0, x + 1, y, z + eps,
- 0, 0, x + 1, y + 1, z + eps,
- 0, 0, x, y + 1, z + eps,
- ]));
- }
-
- // Neighboring chunks are drawn dimmer and semi-transparent, so they
- // read as context rather than part of the chunk being edited.
- const NEIGHBOR_ALPHA = 0.35;
- const NEIGHBOR_DARKEN = 0.5;
-
- // scene = {
- // target: [x, y, z], worldH: number,
- // terrain: { mesh, texture } | null,
- // gridLines: { mesh } | null,
- // models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
- // neighbors: [{
- // offset: [x,y,z],
- // terrain: { mesh, texture } | null,
- // models: [{ mesh, texture, color, offset }],
- // }],
- // highlight: { x, y, z } | null,
- // }
- function render(scene) {
- const width = canvas.width, height = canvas.height;
- gl.viewport(0, 0, width, height);
- gl.clearColor(0.16, 0.18, 0.22, 1);
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
- gl.useProgram(program);
- gl.depthMask(true);
-
- const { view, fov } = computeCamera(scene.target, scene.worldH);
- const projection = mat4Perspective(new Float32Array(16), fov, width / height, 0.1, 1000);
- gl.uniformMatrix4fv(uniforms.uProjection, false, projection);
- gl.uniformMatrix4fv(uniforms.uView, false, view);
-
- const identity = mat4Identity(new Float32Array(16));
-
- if(scene.terrain) {
- drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true);
- }
-
- if(scene.gridLines) {
- drawMesh(scene.gridLines.mesh, identity, null, [0, 0, 0, 0.35], false, gl.LINES);
- }
-
- for(const model of scene.models) {
- const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
- drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
- }
-
- for(const neighbor of scene.neighbors || []) {
- if(neighbor.terrain) {
- const modelMatrix = mat4Translation(new Float32Array(16), neighbor.offset);
- drawMesh(
- neighbor.terrain.mesh, modelMatrix, neighbor.terrain.texture,
- [NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_ALPHA], false,
- );
- }
-
- for(const model of neighbor.models) {
- const worldOffset = [
- neighbor.offset[0] + model.offset[0],
- neighbor.offset[1] + model.offset[1],
- neighbor.offset[2] + model.offset[2],
- ];
- const modelMatrix = mat4Translation(new Float32Array(16), worldOffset);
- const color = [
- model.color[0] * NEIGHBOR_DARKEN,
- model.color[1] * NEIGHBOR_DARKEN,
- model.color[2] * NEIGHBOR_DARKEN,
- model.color[3] * NEIGHBOR_ALPHA,
- ];
- drawMesh(model.mesh, modelMatrix, model.texture, color, false);
- }
- }
-
- if(scene.highlight) {
- const { x, y, z } = scene.highlight;
- const highlightMesh = buildHighlightMesh(x, y, z);
- drawMesh(highlightMesh, identity, null, [1, 0.92, 0.23, 1], false, gl.LINE_LOOP);
- }
- }
-
- return { gl, createMesh, createTextureFromImage, render };
- }
-
- return Object.freeze({ create, computeCamera });
-})();
diff --git a/editor/client/public/template.html b/editor/client/public/template.html
deleted file mode 100644
index 5ea9aba2..00000000
--- a/editor/client/public/template.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
- Dusk Map Editor
-
-
-
-
-
-
-
-
diff --git a/editor/server/server.py b/editor/server/server.py
deleted file mode 100644
index 8985ce34..00000000
--- a/editor/server/server.py
+++ /dev/null
@@ -1,298 +0,0 @@
-"""
-Map Editor HTTP Server
-
-Serves editor/client/public/ as static files with smart routing:
- / -> index.html
- /page -> page.html or page/index.html
- /index -> index.html
-
-API endpoints (all paths relative to project root):
- GET /api/ls?path= directory listing
- GET /api/read?path= file contents
- GET /api/find?path=&ext=<.json> recursive file listing by extension
- POST /api/write?path= write file (JSON body: {"content":"..."})
- POST /api/mkdir?path= create directory
- POST /api/delete?path= delete a file
- POST /api/compile-chunk?x=&y=&z= recompile a chunk JSON to .dcf
-"""
-
-import os
-import re
-import sys
-import json
-import mimetypes
-import subprocess
-from http.server import HTTPServer, BaseHTTPRequestHandler
-from urllib.parse import urlparse, parse_qs, unquote
-from pathlib import Path
-
-CHUNK_COORD_RE = re.compile(r"^-?\d+$")
-
-PORT = 3000
-SCRIPT_DIR = Path(__file__).parent.resolve()
-PUBLIC_DIR = SCRIPT_DIR.parent / "client" / "public"
-PROJECT_ROOT = SCRIPT_DIR.parent.parent.resolve()
-
-
-class Handler(BaseHTTPRequestHandler):
- def do_GET(self):
- parsed = urlparse(self.path)
- path = unquote(parsed.path)
- if path.startswith("/api/"):
- params = parse_qs(parsed.query)
- if path == "/api/ls":
- self.api_ls(params)
- elif path == "/api/read":
- self.api_read(params)
- elif path == "/api/find":
- self.api_find(params)
- else:
- self.send_error(404)
- else:
- self.serve_static(path)
-
- def do_POST(self):
- parsed = urlparse(self.path)
- params = parse_qs(parsed.query)
- if parsed.path == "/api/write":
- self.api_write(params)
- elif parsed.path == "/api/mkdir":
- self.api_mkdir(params)
- elif parsed.path == "/api/delete":
- self.api_delete(params)
- elif parsed.path == "/api/compile-chunk":
- self.api_compile_chunk(params)
- else:
- self.send_error(404)
-
- # ------------------------------------------------------------------
- # Helpers
- # ------------------------------------------------------------------
-
- def resolve_project_path(self, rel):
- rel = rel.lstrip("/")
- target = (PROJECT_ROOT / rel).resolve()
- if not str(target).startswith(str(PROJECT_ROOT)):
- return None
- return target
-
- def send_json(self, data, code=200):
- body = json.dumps(data, indent=2).encode("utf-8")
- self.send_response(code)
- self.send_header("Content-Type", "application/json")
- self.send_header("Content-Length", len(body))
- self.end_headers()
- self.wfile.write(body)
-
- def read_body(self):
- length = int(self.headers.get("Content-Length", 0))
- return self.rfile.read(length)
-
- # ------------------------------------------------------------------
- # API handlers
- # ------------------------------------------------------------------
-
- def api_ls(self, params):
- rel = params.get("path", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
- if not target.exists():
- self.send_json({"error": "path not found"}, 404)
- return
- if not target.is_dir():
- self.send_json({"error": "not a directory"}, 400)
- return
-
- entries = []
- for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name)):
- entries.append({
- "name": entry.name,
- "type": "dir" if entry.is_dir() else "file",
- "size": entry.stat().st_size if entry.is_file() else None,
- })
- self.send_json({
- "path": str(target.relative_to(PROJECT_ROOT)),
- "entries": entries,
- })
-
- def api_read(self, params):
- rel = params.get("path", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
- if not target.exists():
- self.send_json({"error": "file not found"}, 404)
- return
- if not target.is_file():
- self.send_json({"error": "not a file"}, 400)
- return
-
- try:
- content = target.read_text(encoding="utf-8")
- self.send_json({
- "path": str(target.relative_to(PROJECT_ROOT)),
- "content": content,
- })
- except UnicodeDecodeError:
- import base64
- content = base64.b64encode(target.read_bytes()).decode("ascii")
- self.send_json({
- "path": str(target.relative_to(PROJECT_ROOT)),
- "content": content,
- "encoding": "base64",
- })
-
- def api_write(self, params):
- rel = params.get("path", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
-
- body = self.read_body()
- try:
- data = json.loads(body)
- content = data.get("content", "")
- except (json.JSONDecodeError, UnicodeDecodeError):
- content = body.decode("utf-8")
-
- target.parent.mkdir(parents=True, exist_ok=True)
- target.write_text(content, encoding="utf-8")
- self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
-
- def api_mkdir(self, params):
- rel = params.get("path", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
- target.mkdir(parents=True, exist_ok=True)
- self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
-
- def api_delete(self, params):
- rel = params.get("path", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
- if target.is_file():
- target.unlink()
- self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
-
- def api_find(self, params):
- rel = params.get("path", [""])[0]
- ext = params.get("ext", [""])[0]
- target = self.resolve_project_path(rel)
- if target is None:
- self.send_json({"error": "invalid path"}, 400)
- return
- if not target.is_dir():
- self.send_json({"error": "not a directory"}, 400)
- return
-
- pattern = f"*{ext}" if ext else "*"
- paths = sorted(
- str(p.relative_to(target)).replace(os.sep, "/")
- for p in target.rglob(pattern)
- if p.is_file()
- )
- self.send_json({"path": str(target.relative_to(PROJECT_ROOT)), "files": paths})
-
- def api_compile_chunk(self, params):
- x = params.get("x", [""])[0]
- y = params.get("y", [""])[0]
- z = params.get("z", [""])[0]
- if not (CHUNK_COORD_RE.match(x) and CHUNK_COORD_RE.match(y) and CHUNK_COORD_RE.match(z)):
- self.send_json({"error": "invalid chunk coordinates"}, 400)
- return
-
- json_path = (
- PROJECT_ROOT / "assetsraw" / "maps" / "overworld" / "chunks"
- / f"chunk_{x}_{y}_{z}.json"
- )
- if not json_path.is_file():
- self.send_json({"error": "chunk json not found"}, 404)
- return
-
- result = subprocess.run(
- [sys.executable, "-m", "tools.asset.chunk", str(json_path)],
- cwd=str(PROJECT_ROOT),
- capture_output=True,
- text=True,
- )
- self.send_json({
- "ok": result.returncode == 0,
- "returncode": result.returncode,
- "stdout": result.stdout,
- "stderr": result.stderr,
- })
-
- # ------------------------------------------------------------------
- # Static file serving
- # ------------------------------------------------------------------
-
- def serve_static(self, path):
- clean = path.lstrip("/")
- candidates = []
-
- if clean == "" or clean == "index" or clean == "index.html":
- candidates = ["index.html"]
- else:
- candidates.append(clean)
- if "." not in Path(clean).name:
- candidates.append(clean + ".html")
- candidates.append(clean + "/index.html")
-
- for candidate in candidates:
- file_path = PUBLIC_DIR / candidate
- if file_path.is_file():
- self.serve_file(file_path)
- return
-
- self.send_error(404)
-
- def serve_file(self, file_path):
- mime, _ = mimetypes.guess_type(str(file_path))
- mime = mime or "application/octet-stream"
-
- template = PUBLIC_DIR / "template.html"
- if (
- mime == "text/html"
- and file_path.name != "template.html"
- and template.is_file()
- ):
- fragment = file_path.read_text(encoding="utf-8")
- shell = template.read_text(encoding="utf-8")
- marker = shell.find('id="editor"')
- if marker == -1:
- content = shell.encode("utf-8")
- else:
- tag_end = shell.index(">", marker) + 1
- close_idx = shell.index("", tag_end)
- content = (shell[:tag_end] + fragment + shell[close_idx:]).encode("utf-8")
- else:
- content = file_path.read_bytes()
-
- self.send_response(200)
- self.send_header("Content-Type", mime)
- self.send_header("Content-Length", len(content))
- self.end_headers()
- self.wfile.write(content)
-
- def log_message(self, fmt, *args):
- print(f" {self.address_string()} {fmt % args}")
-
-
-if __name__ == "__main__":
- print(f"Map Editor")
- print(f" http://localhost:{PORT}")
- print(f" public : {PUBLIC_DIR}")
- print(f" project: {PROJECT_ROOT}")
- server = HTTPServer(("", PORT), Handler)
- try:
- server.serve_forever()
- except KeyboardInterrupt:
- print("\nStopped.")
diff --git a/editor/start.sh b/editor/start.sh
deleted file mode 100755
index e1871d04..00000000
--- a/editor/start.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-set -e
-cd "$(dirname "$0")"
-python3 server/server.py
diff --git a/scripts/build-gamecube-docker.sh b/scripts/gamecube/build-gamecube-docker.sh
similarity index 78%
rename from scripts/build-gamecube-docker.sh
rename to scripts/gamecube/build-gamecube-docker.sh
index 69d2777b..1af6d466 100755
--- a/scripts/build-gamecube-docker.sh
+++ b/scripts/gamecube/build-gamecube-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
\ No newline at end of file
+docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/gamecube/build-gamecube.sh"
\ No newline at end of file
diff --git a/scripts/build-gamecube-iso-docker.sh b/scripts/gamecube/build-gamecube-iso-docker.sh
similarity index 76%
rename from scripts/build-gamecube-iso-docker.sh
rename to scripts/gamecube/build-gamecube-iso-docker.sh
index 24975feb..4dcea118 100755
--- a/scripts/build-gamecube-iso-docker.sh
+++ b/scripts/gamecube/build-gamecube-iso-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
+docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/gamecube/build-gamecube-iso.sh"
diff --git a/scripts/build-gamecube-iso.sh b/scripts/gamecube/build-gamecube-iso.sh
similarity index 100%
rename from scripts/build-gamecube-iso.sh
rename to scripts/gamecube/build-gamecube-iso.sh
diff --git a/scripts/build-gamecube.sh b/scripts/gamecube/build-gamecube.sh
similarity index 100%
rename from scripts/build-gamecube.sh
rename to scripts/gamecube/build-gamecube.sh
diff --git a/scripts/build-knulli-docker.sh b/scripts/knulli/build-knulli-docker.sh
similarity index 80%
rename from scripts/build-knulli-docker.sh
rename to scripts/knulli/build-knulli-docker.sh
index 26f8bfc6..4deaab17 100755
--- a/scripts/build-knulli-docker.sh
+++ b/scripts/knulli/build-knulli-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
\ No newline at end of file
+docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/knulli/build-knulli.sh"
\ No newline at end of file
diff --git a/scripts/build-knulli.sh b/scripts/knulli/build-knulli.sh
similarity index 100%
rename from scripts/build-knulli.sh
rename to scripts/knulli/build-knulli.sh
diff --git a/scripts/build-linux-docker.sh b/scripts/linux/build-linux-docker.sh
similarity index 81%
rename from scripts/build-linux-docker.sh
rename to scripts/linux/build-linux-docker.sh
index b46fddd9..8ec3e6cc 100755
--- a/scripts/build-linux-docker.sh
+++ b/scripts/linux/build-linux-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
\ No newline at end of file
+docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/linux/build-linux.sh"
\ No newline at end of file
diff --git a/scripts/build-linux.sh b/scripts/linux/build-linux.sh
similarity index 100%
rename from scripts/build-linux.sh
rename to scripts/linux/build-linux.sh
diff --git a/scripts/test-linux-docker.sh b/scripts/linux/test-linux-docker.sh
similarity index 75%
rename from scripts/test-linux-docker.sh
rename to scripts/linux/test-linux-docker.sh
index d5c70904..57adf7ce 100755
--- a/scripts/test-linux-docker.sh
+++ b/scripts/linux/test-linux-docker.sh
@@ -4,4 +4,4 @@ docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
- /bin/bash -c "./scripts/test-linux.sh"
\ No newline at end of file
+ /bin/bash -c "./scripts/linux/test-linux.sh"
\ No newline at end of file
diff --git a/scripts/test-linux.sh b/scripts/linux/test-linux.sh
similarity index 100%
rename from scripts/test-linux.sh
rename to scripts/linux/test-linux.sh
diff --git a/scripts/build-psp-docker.sh b/scripts/psp/build-psp-docker.sh
similarity index 84%
rename from scripts/build-psp-docker.sh
rename to scripts/psp/build-psp-docker.sh
index 348fdce2..13a2d3d0 100755
--- a/scripts/build-psp-docker.sh
+++ b/scripts/psp/build-psp-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
\ No newline at end of file
+docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/psp/build-psp.sh"
\ No newline at end of file
diff --git a/scripts/build-psp.sh b/scripts/psp/build-psp.sh
similarity index 100%
rename from scripts/build-psp.sh
rename to scripts/psp/build-psp.sh
diff --git a/scripts/build-vita-docker.sh b/scripts/vita/build-vita-docker.sh
similarity index 82%
rename from scripts/build-vita-docker.sh
rename to scripts/vita/build-vita-docker.sh
index a0308cb4..733359df 100755
--- a/scripts/build-vita-docker.sh
+++ b/scripts/vita/build-vita-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/vita/build-vita.sh"
diff --git a/scripts/build-vita.sh b/scripts/vita/build-vita.sh
similarity index 100%
rename from scripts/build-vita.sh
rename to scripts/vita/build-vita.sh
diff --git a/scripts/build-wii-docker.sh b/scripts/wii/build-wii-docker.sh
similarity index 83%
rename from scripts/build-wii-docker.sh
rename to scripts/wii/build-wii-docker.sh
index 087f551a..6bfbdc88 100755
--- a/scripts/build-wii-docker.sh
+++ b/scripts/wii/build-wii-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
\ No newline at end of file
+docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/wii/build-wii.sh"
\ No newline at end of file
diff --git a/scripts/build-wii-iso-docker.sh b/scripts/wii/build-wii-iso-docker.sh
similarity index 80%
rename from scripts/build-wii-iso-docker.sh
rename to scripts/wii/build-wii-iso-docker.sh
index cd05520a..cde6ba70 100755
--- a/scripts/build-wii-iso-docker.sh
+++ b/scripts/wii/build-wii-iso-docker.sh
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
-docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
+docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/wii/build-wii-iso.sh"
diff --git a/scripts/build-wii-iso.sh b/scripts/wii/build-wii-iso.sh
similarity index 100%
rename from scripts/build-wii-iso.sh
rename to scripts/wii/build-wii-iso.sh
diff --git a/scripts/build-wii.sh b/scripts/wii/build-wii.sh
similarity index 100%
rename from scripts/build-wii.sh
rename to scripts/wii/build-wii.sh
diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt
index c0347f89..b8c57116 100644
--- a/src/dusk/CMakeLists.txt
+++ b/src/dusk/CMakeLists.txt
@@ -81,11 +81,9 @@ add_subdirectory(error)
add_subdirectory(input)
add_subdirectory(locale)
add_subdirectory(perf)
-add_subdirectory(rpg)
add_subdirectory(scene)
add_subdirectory(system)
add_subdirectory(time)
-add_subdirectory(ui)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
\ No newline at end of file
diff --git a/src/dusk/animation/CMakeLists.txt b/src/dusk/animation/CMakeLists.txt
index a03d91ad..db67ed5e 100644
--- a/src/dusk/animation/CMakeLists.txt
+++ b/src/dusk/animation/CMakeLists.txt
@@ -6,6 +6,4 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
- animation.c
- keyframe.c
)
diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c
deleted file mode 100644
index 65fa2b20..00000000
--- a/src/dusk/animation/animation.c
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-#include "animation.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/math.h"
-
-void animationInit(
- animation_t *anim,
- keyframe_t *keyframes,
- uint16_t *keyframeCounts,
- const uint16_t layerCount
-) {
- assertNotNull(anim, "Animation pointer cannot be null.");
- assertNotNull(keyframes, "Keyframes pointer cannot be null.");
- assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
- assertTrue(layerCount > 0, "Layer count must be greater than zero.");
-
- memoryZero(anim, sizeof(animation_t));
- anim->keyframes = keyframes;
- anim->keyframeCounts = keyframeCounts;
- anim->layerCount = layerCount;
-
- // Determine duration
- float_t duration = 0.0f;
- for(uint16_t layer = 0; layer < layerCount; layer++) {
- uint16_t keyframeCount = keyframeCounts[layer];
- assertTrue(keyframeCount > 0, "Keyframe count invalid.");
- keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
-
- #ifdef DUSK_ASSERTIONS
- // Check that the keyframes are sorted by time.
- for(uint16_t i = 1; i < keyframeCount; i++) {
- assertTrue(
- layerKeyframes[i].time >= layerKeyframes[i - 1].time,
- "Keyframes must be sorted by time."
- );
- }
- #endif
-
- keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
- duration = mathMax(duration, lastKeyframe->time);
- }
- assertTrue(duration > 0, "Animation duration must be greater than 0.");
- anim->duration = duration;
-}
-
-float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
- assertNotNull(anim, "Animation pointer cannot be null.");
- assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
-
- uint16_t keyframeCount = anim->keyframeCounts[layer];
- keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
- return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
-}
-
-void animationUpdate(
- animation_t *anim,
- const float_t deltaTime
-) {
- assertNotNull(anim, "Animation pointer cannot be null.");
- assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
-
- bool_t justCompleted = false;
-
- if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
- bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
- bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
- assertFalse(
- loop && pingpong,
- "Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
- );
-
- bool_t backward;
- if(pingpong) {
- backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
- } else {
- backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
- }
-
- // Resolve boundary crossings one at a time, so a single large deltaTime
- // can correctly loop/pingpong across multiple boundaries in one call.
- float_t remaining = deltaTime;
- while(remaining > 0.0f) {
- float_t toBoundary = (
- backward ? anim->time : (anim->duration - anim->time)
- );
- if(remaining < toBoundary) {
- anim->time += backward ? -remaining : remaining;
- break;
- }
-
- remaining -= toBoundary;
- anim->time = backward ? 0.0f : anim->duration;
-
- bool_t stopHere;
- if(backward) {
- stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
- } else {
- stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
- }
-
- if(stopHere) {
- justCompleted = true;
- break;
- } else if(pingpong) {
- backward = !backward;
- if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
- else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
- } else if(loop) {
- anim->time = backward ? anim->duration : 0.0f;
- if(anim->onLoop) anim->onLoop(anim->user);
- } else {
- justCompleted = true;
- break;
- }
- }
-
- if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
- }
-
- // Call onUpdate for each layer.
- for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
- float_t value = animationGetLayerValue(anim, layer);
- if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
- }
-
- if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
-}
\ No newline at end of file
diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h
deleted file mode 100644
index f8cea8b8..00000000
--- a/src/dusk/animation/animation.h
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-#pragma once
-#include "keyframe.h"
-
-#define ANIMATION_FLAG_LOOP (1 << 0)
-#define ANIMATION_FLAG_REVERSE (1 << 1)
-#define ANIMATION_FLAG_PINGPONG (1 << 2)
-#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
-#define ANIMATION_FLAG_STOP_END (1 << 4)
-
-// Internal - tracks which direction a pingponging animation is currently
-// travelling. Do not set this manually, it is managed by animationUpdate().
-#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
-
-// Internal - set once the animation has stopped advancing (see
-// animationUpdate()). Do not set this manually. There is currently no way to
-// restart a completed animation short of clearing this bit and resetting
-// anim->time by hand.
-#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
-
-typedef struct {
- keyframe_t *keyframes;
- uint16_t *keyframeCounts;
- uint16_t layerCount;
- float_t time;
- float_t duration;
- uint8_t flags;
-
- void *user;
- void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
- void (*onComplete)(void *user);
- void (*onLoop)(void *user);
-} animation_t;
-
-/**
- * Initializes an animation with the given keyframes and layer count.
- *
- * @param anim Pointer to the animation to initialize.
- * @param keyframes Pointer to the array of keyframes for each layer.
- * @param keyframeCount Number of keyframes in each layer.
- * @param layerCount Number of layers in the animation.
- */
-void animationInit(
- animation_t *anim,
- keyframe_t *keyframes,
- uint16_t *keyframeCounts,
- const uint16_t layerCount
-);
-
-/**
- * Sets the current time of the animation, clamping it to the valid range.
- * This will call the onUpdate callback but none of the other callbacks.
- *
- * @param anim Pointer to the animation to set the time for.
- * @param time The new time to set for the animation.
- */
-void animationSetTime(animation_t *anim, const float_t time);
-
-/**
- * Gets the current value of a specific layer in the animation based on the
- * current animation time.
- */
-float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
-
-/**
- * Updates the animation state based on the elapsed time. Advances anim->time
- * by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
- * resolves whatever happens when it reaches the 0 or duration boundary:
- *
- * - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
- * in the opposite direction, forever, unless stopped (see below).
- * - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
- * playing in the same direction, forever, unless stopped (see below).
- * - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
- * animation reaches that specific boundary, it clamps there and stops
- * (firing onComplete) instead of looping/pingponging past it.
- * - If none of the above apply at a boundary, the animation clamps there and
- * stops, firing onComplete.
- *
- * onUpdate is called for every layer on every call. onLoop is called each
- * time a loop wraps around. onComplete is called at most once, the moment
- * the animation stops advancing.
- *
- * @param anim Pointer to the animation to update.
- * @param deltaTime Time elapsed since the last update (in seconds).
- */
-void animationUpdate(
- animation_t *anim,
- const float_t deltaTime
-);
\ No newline at end of file
diff --git a/src/dusk/animation/keyframe.c b/src/dusk/animation/keyframe.c
deleted file mode 100644
index a50338ef..00000000
--- a/src/dusk/animation/keyframe.c
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "keyframe.h"
-#include "assert/assert.h"
-#include "util/math.h"
-
-float_t keyframeGetValue(
- const keyframe_t *keyframes,
- const uint32_t keyframeCount,
- const float_t time
-) {
- assertNotNull(keyframes, "Keyframes pointer cannot be null.");
- assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
- assertTrue(time >= 0, "Time must be non-negative.");
- #ifdef DUSK_ASSERTIONS
- // Checks that the keyframes are sorted by time.
- for(uint32_t i = 1; i < keyframeCount; i++) {
- assertTrue(
- keyframes[i].time >= keyframes[i - 1].time,
- "Keyframes must be sorted by time."
- );
- }
- #endif
-
- keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
- if(time >= last->time) return last->value;
-
- // Since time < last->time (checked above), current is guaranteed to stop
- // at or before reaching last, so no separate end-of-array check is needed.
- keyframe_t *current = (keyframe_t *)keyframes;
- keyframe_t *start = current;
- while(current->time <= time) {
- start = current;
- current++;
- }
- keyframe_t *end = current;
-
- float_t t = (time - start->time) / (end->time - start->time);
- return mathLerp(start->value, end->value, easingApply(start->easing, t));
-}
\ No newline at end of file
diff --git a/src/dusk/animation/keyframe.h b/src/dusk/animation/keyframe.h
deleted file mode 100644
index 96a0a4f6..00000000
--- a/src/dusk/animation/keyframe.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2026 Dominic Masters
-//
-// This software is released under the MIT License.
-// https://opensource.org/licenses/MIT
-
-#pragma once
-#include "easing.h"
-
-typedef struct {
- float_t time;
- float_t value;
- easingtype_t easing;
-} keyframe_t;
-
-/**
- * Gets the value of a keyframe at a given time.
- *
- * @param keyframes The keyframes to get the value from.
- * @param keyframeCount The number of keyframes in the array.
- * @param time The time at which to get the value, in seconds.
- * @return The value of the keyframe at the given time.
- */
-float_t keyframeGetValue(
- const keyframe_t *keyframes,
- const uint32_t keyframeCount,
- const float_t time
-);
\ No newline at end of file
diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt
index 21932838..5ef3dccf 100644
--- a/src/dusk/asset/loader/CMakeLists.txt
+++ b/src/dusk/asset/loader/CMakeLists.txt
@@ -15,7 +15,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
-add_subdirectory(chunk)
add_subdirectory(dmf)
add_subdirectory(wav)
add_subdirectory(mp3)
\ No newline at end of file
diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c
index ad808032..c66a5f28 100644
--- a/src/dusk/asset/loader/assetloader.c
+++ b/src/dusk/asset/loader/assetloader.c
@@ -46,12 +46,6 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.dispose = assetJsonDispose
},
- [ASSET_LOADER_TYPE_CHUNK] = {
- .loadSync = assetChunkLoaderSync,
- .loadAsync = assetChunkLoaderAsync,
- .dispose = assetChunkDispose
- },
-
[ASSET_LOADER_TYPE_WAV] = {
.loadSync = assetWavLoaderSync,
.loadAsync = assetWavLoaderAsync,
diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h
index c18c108f..ae339546 100644
--- a/src/dusk/asset/loader/assetloader.h
+++ b/src/dusk/asset/loader/assetloader.h
@@ -12,7 +12,6 @@
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
-#include "asset/loader/chunk/assetchunkloader.h"
#include "asset/loader/wav/assetwavloader.h"
#include "asset/loader/mp3/assetmp3loader.h"
@@ -25,7 +24,6 @@ typedef enum {
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
- ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_WAV,
ASSET_LOADER_TYPE_MP3,
@@ -39,7 +37,6 @@ typedef union {
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
- assetchunkloaderloading_t chunk;
assetwavloaderloading_t wav;
assetmp3loaderloading_t mp3;
} assetloaderloading_t;
@@ -51,7 +48,6 @@ typedef union {
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
- assetchunkoutput_t chunk;
assetwavoutput_t wav;
assetmp3output_t mp3;
} assetloaderoutput_t;
@@ -61,7 +57,6 @@ typedef union {
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
- assetchunkloaderinput_t chunk;
assetwavloaderinput_t wav;
assetmp3loaderinput_t mp3;
} assetloaderinput_t;
diff --git a/src/dusk/asset/loader/chunk/CMakeLists.txt b/src/dusk/asset/loader/chunk/CMakeLists.txt
deleted file mode 100644
index 64e16290..00000000
--- a/src/dusk/asset/loader/chunk/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- assetchunkloader.c
-)
diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c
deleted file mode 100644
index a2325027..00000000
--- a/src/dusk/asset/loader/chunk/assetchunkloader.c
+++ /dev/null
@@ -1,484 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "assetchunkloader.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloading.h"
-#include "asset/loader/assetentry.h"
-#include "asset/loader/assetloader.h"
-#include "asset/loader/json/assetjsonloader.h"
-#include "display/color.h"
-#include "display/texture/texture.h"
-#include "rpg/overworld/worldpos.h"
-#include "yyjson.h"
-
-#define ASSET_CHUNK_TERRAIN_TEXTURE "tiles.png"
-
-// Per-shape corner Z offsets (sw, se, ne, nw) as offsets from a tile's own
-// base Z, ported 1:1 from tools/asset/chunk/__main__.py's _RAMP_CORNERS.
-// TILE_SHAPE_NULL's row is never read - assetChunkTilesHaveTerrain/
-// assetChunkBuildTerrain skip those tiles outright.
-static const float_t CHUNK_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
- [TILE_SHAPE_GROUND] = {0.0f, 0.0f, 0.0f, 0.0f},
- [TILE_SHAPE_RAMP_NORTH] = {0.0f, 0.0f, 1.0f, 1.0f},
- [TILE_SHAPE_RAMP_SOUTH] = {1.0f, 1.0f, 0.0f, 0.0f},
- [TILE_SHAPE_RAMP_EAST] = {0.0f, 1.0f, 1.0f, 0.0f},
- [TILE_SHAPE_RAMP_WEST] = {1.0f, 0.0f, 0.0f, 1.0f},
- [TILE_SHAPE_RAMP_NORTHEAST] = {0.0f, 0.0f, 1.0f, 0.0f},
- [TILE_SHAPE_RAMP_NORTHWEST] = {0.0f, 0.0f, 0.0f, 1.0f},
- [TILE_SHAPE_RAMP_SOUTHEAST] = {0.0f, 1.0f, 0.0f, 0.0f},
- [TILE_SHAPE_RAMP_SOUTHWEST] = {1.0f, 0.0f, 0.0f, 0.0f},
- [TILE_SHAPE_RAMP_NORTHEAST_INNER] = {0.0f, 1.0f, 1.0f, 1.0f},
- [TILE_SHAPE_RAMP_NORTHWEST_INNER] = {1.0f, 0.0f, 1.0f, 1.0f},
- [TILE_SHAPE_RAMP_SOUTHEAST_INNER] = {1.0f, 1.0f, 1.0f, 0.0f},
- [TILE_SHAPE_RAMP_SOUTHWEST_INNER] = {1.0f, 1.0f, 0.0f, 1.0f},
-};
-
-errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
- assertNotNull(loading, "Loading cannot be NULL");
- assertNotMainThread("Should be called from an async thread.");
- errorOk();
-}
-
-static bool_t assetChunkTilesHaveTerrain(const tile_t *tiles) {
- for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
- if(tiles[i].shape != TILE_SHAPE_NULL) return true;
- }
- return false;
-}
-
-// Reads the "tiles" array - a dense, fixed-size CHUNK_TILE_COUNT grid,
-// one slot per (x, y) column at index x + y * CHUNK_WIDTH (position is
-// implicit in the index, not stored). Each slot is either `null` (empty
-// column, already zeroed = TILE_SHAPE_NULL) or a 2-element [type, z]
-// array.
-static void assetChunkParseTiles(yyjson_val *root, tile_t *tiles) {
- memoryZero(tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
-
- yyjson_val *tilesArr = yyjson_obj_get(root, "tiles");
- if(!yyjson_is_arr(tilesArr)) return;
-
- size_t idx, len;
- yyjson_val *tileVal;
- yyjson_arr_foreach(tilesArr, idx, len, tileVal) {
- if(idx >= CHUNK_TILE_COUNT) break;
- if(!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) < 2) continue;
-
- int32_t type = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 0));
- int32_t z = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 1));
- tiles[idx] = (tile_t){ .shape = (tileshape_t)type, .z = (uint8_t)z };
- }
-}
-
-// Resolves "meshes" (external building references, by bare filename) into
-// out->modelNames/meshOffsets starting at *ioIndex, advancing it as
-// entries are written. Filenames are resolved as
-// "models/buildings/.json" - a hardcoded stand-in for
-// tools/asset/chunk/__main__.py's find_model() recursive search, valid
-// as long as every building model lives flat under that one directory
-// (true today - see the raw-json-maps plan notes for the tradeoff).
-static void assetChunkParseMeshes(
- yyjson_val *root, assetchunkoutput_t *out, uint8_t *ioIndex
-) {
- yyjson_val *meshesArr = yyjson_obj_get(root, "meshes");
- if(!yyjson_is_arr(meshesArr)) return;
-
- size_t idx, len;
- yyjson_val *meshVal;
- yyjson_arr_foreach(meshesArr, idx, len, meshVal) {
- if(*ioIndex >= CHUNK_MESH_COUNT_MAX) break;
-
- yyjson_val *fileVal = yyjson_obj_get(meshVal, "file");
- if(!fileVal || !yyjson_is_str(fileVal)) continue;
-
- char_t baseName[CHUNK_MESH_NAME_MAX];
- stringCopy(baseName, yyjson_get_str(fileVal), sizeof(baseName));
- char_t *dot = stringFindLastChar(baseName, '.');
- if(dot != NULL) *dot = '\0';
-
- uint8_t m = *ioIndex;
- stringFormat(
- out->modelNames[m], CHUNK_MESH_NAME_MAX,
- "models/buildings/%s.json", baseName
- );
-
- vec3 offset = {0.0f, 0.0f, 0.0f};
- yyjson_val *posArr = yyjson_obj_get(meshVal, "pos");
- if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) {
- offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0));
- offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1));
- offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2));
- }
- memoryCopy(out->meshOffsets[m], offset, sizeof(vec3));
-
- (*ioIndex)++;
- }
-}
-
-static void assetChunkParseEntities(yyjson_val *root, assetchunkoutput_t *out) {
- out->entitySpawnCount = 0;
-
- yyjson_val *arr = yyjson_obj_get(root, "entities");
- if(!yyjson_is_arr(arr)) return;
-
- size_t idx, len;
- yyjson_val *val;
- yyjson_arr_foreach(arr, idx, len, val) {
- if(out->entitySpawnCount >= CHUNK_ENTITY_SPAWN_COUNT_MAX) break;
-
- const char_t *type = yyjson_get_str(yyjson_obj_get(val, "type"));
- yyjson_val *posArr = yyjson_obj_get(val, "pos");
- if(type == NULL || !yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) {
- continue;
- }
- worldpos_t pos = {
- .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 0)),
- .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 1)),
- .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 2)),
- };
-
- chunkentityspawn_t *spawn = &out->entitySpawns[out->entitySpawnCount];
- if(stringEquals(type, "global")) {
- spawn->kind = CHUNK_ENTITY_SPAWN_KIND_GLOBAL;
- spawn->globalId =
- (uint16_t)yyjson_get_int(yyjson_obj_get(val, "globalId"));
- spawn->itemId = 0;
- spawn->itemQuantity = 0;
- } else if(stringEquals(type, "item")) {
- spawn->kind = CHUNK_ENTITY_SPAWN_KIND_ITEM;
- spawn->globalId = 0;
- spawn->itemId = (uint16_t)yyjson_get_int(yyjson_obj_get(val, "itemId"));
- spawn->itemQuantity =
- (uint8_t)yyjson_get_int(yyjson_obj_get(val, "quantity"));
- } else {
- continue;
- }
- spawn->position = pos;
- out->entitySpawnCount++;
- }
-}
-
-static void assetChunkParseAreas(yyjson_val *root, assetchunkoutput_t *out) {
- out->areaSpawnCount = 0;
-
- yyjson_val *arr = yyjson_obj_get(root, "areas");
- if(!yyjson_is_arr(arr)) return;
-
- size_t idx, len;
- yyjson_val *val;
- yyjson_arr_foreach(arr, idx, len, val) {
- if(out->areaSpawnCount >= CHUNK_AREA_COUNT_MAX) break;
-
- yyjson_val *minArr = yyjson_obj_get(val, "min");
- yyjson_val *maxArr = yyjson_obj_get(val, "max");
- if(
- !yyjson_is_arr(minArr) || yyjson_arr_size(minArr) < 3 ||
- !yyjson_is_arr(maxArr) || yyjson_arr_size(maxArr) < 3
- ) continue;
-
- chunkareaspawn_t *area = &out->areaSpawns[out->areaSpawnCount];
- area->min = (worldpos_t){
- .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 0)),
- .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 1)),
- .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 2)),
- };
- area->max = (worldpos_t){
- .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 0)),
- .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 1)),
- .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 2)),
- };
- area->callbackId =
- (uint16_t)yyjson_get_int(yyjson_obj_get(val, "callbackId"));
- area->notify = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "notify"));
- area->trigger = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "trigger"));
- out->areaSpawnCount++;
- }
-}
-
-// Six vertices (2 CCW triangles) for one tile's quad, matching
-// tools/asset/chunk/__main__.py's _tile_quad. Corner order: SW=(x,y),
-// SE=(x+1,y), NE=(x+1,y+1), NW=(x,y+1). NORTH=+Y, EAST=+X.
-static void assetChunkAppendTileQuad(
- meshvertex_t *verts, uint32_t *vertCount,
- const uint32_t x, const uint32_t y,
- const float_t sw, const float_t se, const float_t ne, const float_t nw
-) {
- const float_t u0 = (float_t)x / (float_t)CHUNK_WIDTH;
- const float_t u1 = (float_t)(x + 1) / (float_t)CHUNK_WIDTH;
- const float_t v0 = (float_t)y / (float_t)CHUNK_HEIGHT;
- const float_t v1 = (float_t)(y + 1) / (float_t)CHUNK_HEIGHT;
- const float_t fx = (float_t)x;
- const float_t fy = (float_t)y;
-
- meshvertex_t *v = verts + *vertCount;
- v[0] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
- v[1] = (meshvertex_t){ .uv = {u1, v0}, .pos = {fx + 1, fy, se} };
- v[2] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
- v[3] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
- v[4] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
- v[5] = (meshvertex_t){ .uv = {u0, v1}, .pos = {fx, fy + 1, nw} };
- *vertCount += 6;
-}
-
-// Builds a mesh_t straight from the tile grid - one quad per non-null
-// tile column, shaped by CHUNK_RAMP_CORNERS - the runtime equivalent of
-// tools/asset/chunk/__main__.py's build_terrain_verts. Only called once
-// assetChunkTilesHaveTerrain has confirmed at least one real tile exists.
-// Mirrors assetMeshLoaderSync's meshInit/meshFlush/free-after-upload
-// sequence (assetmeshloader.c) since this produces the same
-// assetmeshoutput_t shape, just from generated verts instead of a file.
-static errorret_t assetChunkBuildTerrain(
- const tile_t *tiles, assetmeshoutput_t *out
-) {
- meshvertex_t *verts = memoryAllocate(
- CHUNK_TILE_COUNT * 6 * sizeof(meshvertex_t)
- );
- uint32_t vertCount = 0;
-
- for(uint32_t y = 0; y < CHUNK_HEIGHT; y++) {
- for(uint32_t x = 0; x < CHUNK_WIDTH; x++) {
- const tile_t *tile = &tiles[x + y * CHUNK_WIDTH];
- if(tile->shape == TILE_SHAPE_NULL) continue;
-
- const float_t *corners = CHUNK_RAMP_CORNERS[tile->shape];
- const float_t fz = (float_t)tile->z;
- assetChunkAppendTileQuad(
- verts, &vertCount, x, y,
- (fz + corners[0]) * WORLD_LAYER_HEIGHT,
- (fz + corners[1]) * WORLD_LAYER_HEIGHT,
- (fz + corners[2]) * WORLD_LAYER_HEIGHT,
- (fz + corners[3]) * WORLD_LAYER_HEIGHT
- );
- }
- }
-
- assertTrue(vertCount > 0, "Terrain build called with no terrain tiles.");
-
- out->vertices = verts;
- errorret_t ret = meshInit(
- &out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, (int32_t)vertCount, verts
- );
- if(errorIsNotOk(ret)) {
- memoryFree(out->vertices);
- out->vertices = NULL;
- errorChain(ret);
- }
- out->meshInitialized = true;
-
- ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
- if(errorIsNotOk(ret)) {
- meshDispose(&out->mesh);
- out->meshInitialized = false;
- memoryFree(out->vertices);
- out->vertices = NULL;
- errorChain(ret);
- }
-
- #if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
- memoryFree(out->vertices);
- out->vertices = NULL;
- #endif
-
- errorOk();
-}
-
-errorret_t assetChunkLoaderSync(assetloading_t *loading) {
- assertNotNull(loading, "Loading cannot be NULL");
- assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
- assertIsMainThread("Must be called from the main thread.");
-
- assetchunkoutput_t *out = &loading->entry->data.chunk;
-
- switch(loading->loading.chunk.state) {
- case ASSET_CHUNK_LOADING_STATE_INITIAL:
- loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
- loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
- errorOk();
-
- case ASSET_CHUNK_LOADING_STATE_PARSE:
- break;
-
- case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
- while(loading->loading.chunk.modelIndex < out->meshCount) {
- uint8_t m = loading->loading.chunk.modelIndex;
- if(out->modelEntries[m] == NULL) {
- out->modelEntries[m] = assetLock(
- out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
- );
- assertNotNull(out->modelEntries[m], "Failed to lock model.");
- loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
- errorOk();
- }
- if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) {
- assetLoaderErrorThrow(
- loading, "Model failed to load: %s", out->modelNames[m]
- );
- }
- if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
- loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
- errorOk();
- }
- loading->loading.chunk.modelIndex++;
- }
- loading->entry->state = ASSET_ENTRY_STATE_LOADED;
- errorOk();
-
- default:
- errorOk();
- }
-
- // --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly
- // like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does
- // for its model descriptor, then read everything out of it before
- // unlocking (the yyjson_doc/vals only stay valid while jsonEntry does).
- char_t jsonKey[ASSET_FILE_NAME_MAX];
- stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name);
- assetloaderinput_t jsonInput;
- memoryZero(&jsonInput, sizeof(jsonInput));
- stringCopy(
- jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
- );
- assetentry_t *jsonEntry =
- assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput);
- errorret_t jsonRet = assetRequireLoaded(jsonEntry);
- if(errorIsNotOk(jsonRet)) {
- assetUnlockEntry(jsonEntry);
- assetLoaderErrorThrow(
- loading, "Failed to load chunk JSON: %s", loading->entry->name
- );
- }
-
- yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
-
- tile_t tiles[CHUNK_TILE_COUNT];
- assetChunkParseTiles(root, tiles);
- out->hasTerrain = assetChunkTilesHaveTerrain(tiles);
-
- uint8_t meshIndex = out->hasTerrain ? 1 : 0;
- assetChunkParseMeshes(root, out, &meshIndex);
- out->meshCount = meshIndex;
-
- assetChunkParseEntities(root, out);
- assetChunkParseAreas(root, out);
-
- assetUnlockEntry(jsonEntry);
-
- out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
- memoryCopy(out->tiles, tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
-
- // Terrain: build the mesh, then wrap it in synthetic (non-cache,
- // non-refcounted) mesh/model entries so modelEntries[0] behaves exactly
- // like a real file-backed model entry for every downstream consumer -
- // see the header's doc comment on hasTerrain/terrainMeshEntry.
- if(out->hasTerrain) {
- assetmeshoutput_t terrainMesh;
- memoryZero(&terrainMesh, sizeof(terrainMesh));
- errorret_t terrainRet = assetChunkBuildTerrain(out->tiles, &terrainMesh);
- if(errorIsNotOk(terrainRet)) {
- memoryFree(out->tiles);
- out->tiles = NULL;
- assetLoaderErrorThrow(
- loading, "Failed to build chunk terrain mesh: %s",
- loading->entry->name
- );
- }
-
- assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
- assetentry_t *texEntry = assetLock(
- ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput
- );
- errorret_t texRet = assetRequireLoaded(texEntry);
- if(errorIsNotOk(texRet)) {
- assetUnlockEntry(texEntry);
- meshDispose(&terrainMesh.mesh);
- memoryFree(out->tiles);
- out->tiles = NULL;
- assetLoaderErrorThrow(
- loading, "Failed to load chunk terrain texture: %s",
- loading->entry->name
- );
- }
- out->terrainTexEntry = texEntry;
-
- out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t));
- memoryZero(out->terrainMeshEntry, sizeof(assetentry_t));
- out->terrainMeshEntry->type = ASSET_LOADER_TYPE_MESH;
- out->terrainMeshEntry->state = ASSET_ENTRY_STATE_LOADED;
- out->terrainMeshEntry->data.mesh = terrainMesh;
-
- out->terrainModelEntry = memoryAllocate(sizeof(assetentry_t));
- memoryZero(out->terrainModelEntry, sizeof(assetentry_t));
- out->terrainModelEntry->type = ASSET_LOADER_TYPE_MODEL;
- out->terrainModelEntry->state = ASSET_ENTRY_STATE_LOADED;
- out->terrainModelEntry->data.model.meshEntry = out->terrainMeshEntry;
- out->terrainModelEntry->data.model.texEntry = out->terrainTexEntry;
- out->terrainModelEntry->data.model.color = COLOR_WHITE;
-
- out->modelEntries[0] = out->terrainModelEntry;
- }
-
- if(out->meshCount == 0) {
- loading->entry->state = ASSET_ENTRY_STATE_LOADED;
- errorOk();
- }
-
- loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
- loading->loading.chunk.modelIndex = out->hasTerrain ? 1 : 0;
- loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
- errorOk();
-}
-
-errorret_t assetChunkDispose(assetentry_t *entry) {
- assertNotNull(entry, "Entry cannot be NULL");
- assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
- assertIsMainThread("Must be called from the main thread.");
-
- assetchunkoutput_t *out = &entry->data.chunk;
-
- if(out->tiles != NULL) {
- memoryFree(out->tiles);
- out->tiles = NULL;
- }
-
- for(uint8_t m = 0; m < out->meshCount; m++) {
- if(out->modelEntries[m] == NULL) continue;
- if(out->hasTerrain && m == 0) continue; // synthetic - freed below.
- assetUnlockEntry(out->modelEntries[m]);
- out->modelEntries[m] = NULL;
- }
-
- if(out->hasTerrain) {
- if(out->terrainMeshEntry != NULL) {
- assetmeshoutput_t *meshOut = &out->terrainMeshEntry->data.mesh;
- if(meshOut->meshInitialized) {
- meshDispose(&meshOut->mesh);
- meshOut->meshInitialized = false;
- }
- if(meshOut->vertices != NULL) {
- memoryFree(meshOut->vertices);
- meshOut->vertices = NULL;
- }
- memoryFree(out->terrainMeshEntry);
- out->terrainMeshEntry = NULL;
- }
- if(out->terrainModelEntry != NULL) {
- memoryFree(out->terrainModelEntry);
- out->terrainModelEntry = NULL;
- }
- if(out->terrainTexEntry != NULL) {
- assetUnlockEntry(out->terrainTexEntry);
- out->terrainTexEntry = NULL;
- }
- out->modelEntries[0] = NULL;
- out->hasTerrain = false;
- }
-
- errorOk();
-}
diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h
deleted file mode 100644
index 38e76032..00000000
--- a/src/dusk/asset/loader/chunk/assetchunkloader.h
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "asset/assetfile.h"
-#include "rpg/overworld/chunk.h"
-
-typedef struct assetloading_s assetloading_t;
-typedef struct assetentry_s assetentry_t;
-
-typedef struct {
- void *nothing;
-} assetchunkloaderinput_t;
-
-typedef enum {
- ASSET_CHUNK_LOADING_STATE_INITIAL,
- ASSET_CHUNK_LOADING_STATE_PARSE,
- ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
- ASSET_CHUNK_LOADING_STATE_DONE
-} assetchunkloadingstate_t;
-
-typedef struct {
- assetchunkloadingstate_t state;
-
- // Index into assetchunkoutput_t.modelNames/modelEntries the external
- // (non-terrain) model polling loop has reached - see
- // ASSET_CHUNK_LOADING_STATE_LOAD_MODELS.
- uint8_t modelIndex;
-} assetchunkloaderloading_t;
-
-typedef enum {
- CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
- CHUNK_ENTITY_SPAWN_KIND_ITEM
-} chunkentityspawnkind_t;
-
-typedef struct {
- chunkentityspawnkind_t kind;
- uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
- uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
- uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
- worldpos_t position;
-} chunkentityspawn_t;
-
-typedef struct {
- worldpos_t min;
- worldpos_t max;
- uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
- uint8_t notify;
- uint8_t trigger;
-} chunkareaspawn_t;
-
-typedef struct {
- tile_t *tiles;
- uint8_t meshCount;
- char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
- vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
- assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
-
- // Terrain mesh generated at load time from the tile grid (no baked
- // .dmf/.json - see assetChunkBuildTerrain in assetchunkloader.c). NULL
- // (with hasTerrain false) for a chunk with no non-null tiles, in which
- // case modelEntries[0] (if meshCount > 0) is the first external mesh
- // instead - matches the old baked pipeline's "only write a terrain
- // model if it has vertices" behavior.
- //
- // terrainMeshEntry/terrainModelEntry are heap-allocated, plain
- // assetentry_t values that are never passed to assetLock/assetGetEntry/
- // assetUnlockEntry (only a real, name-keyed, ref-counted entry may be)
- // - they exist purely so modelEntries[0] can point at something with
- // the same ->state/->data.model/->data.mesh.mesh shape every other
- // model/mesh entry has, so chunk.c/map.c/sceneoverworld.c don't need to
- // know terrain isn't file-backed. Freed directly (not assetUnlockEntry)
- // in assetChunkDispose. terrainTexEntry is a real, ref-counted texture
- // entry (the shared "tiles.png") and must be assetUnlockEntry'd there.
- bool_t hasTerrain;
- assetentry_t *terrainMeshEntry;
- assetentry_t *terrainModelEntry;
- assetentry_t *terrainTexEntry;
-
- uint8_t entitySpawnCount;
- chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
-
- uint8_t areaSpawnCount;
- chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
-} assetchunkoutput_t;
-
-/**
- * Async loader stub for chunk assets. Chunk loading is performed entirely
- * on the main thread (it delegates to the JSON asset loader and to
- * meshInit/meshFlush, both main-thread-only); this callback is never
- * reached.
- *
- * @param loading Loading information for the asset being loaded.
- * @return Error code indicating success or failure of the load operation.
- */
-errorret_t assetChunkLoaderAsync(assetloading_t *loading);
-
-/**
- * Synchronous loader for chunk assets. Locks the chunk's JSON descriptor
- * as a sub-asset, parses tiles/external mesh references/entity spawns/
- * area spawns, generates the terrain mesh directly from the tile grid,
- * and locks/waits for every referenced model (and the terrain's shared
- * tileset texture) to finish loading.
- *
- * @param loading Loading information for the asset being loaded.
- * @return Error code indicating success or failure of the load operation.
- */
-errorret_t assetChunkLoaderSync(assetloading_t *loading);
-
-/**
- * Disposer for chunk assets.
- *
- * @param entry Asset entry containing the chunk data to dispose.
- * @return Error code indicating success or failure of the dispose operation.
- */
-errorret_t assetChunkDispose(assetentry_t *entry);
diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c
index 98596d8e..7e7e07ca 100644
--- a/src/dusk/display/display.c
+++ b/src/dusk/display/display.c
@@ -16,7 +16,6 @@
#include "display/mesh/capsule.h"
#include "display/mesh/triprism.h"
#include "display/screen/screen.h"
-#include "ui/ui.h"
#include "display/text/text.h"
#include "assert/assert.h"
#include "util/memory.h"
diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c
index f1420c4a..674eb357 100644
--- a/src/dusk/engine/engine.c
+++ b/src/dusk/engine/engine.c
@@ -11,11 +11,9 @@
#include "time/time.h"
#include "input/input.h"
#include "locale/localemanager.h"
-#include "rpg/rpg.h"
#include "display/display.h"
#include "scene/scene.h"
#include "asset/asset.h"
-#include "ui/ui.h"
#include "assert/assert.h"
#ifdef DUSK_NETWORK
#include "network/network.h"
@@ -47,8 +45,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(localeManagerInit());
errorChain(displayInit());
errorChain(audioInit());
- errorChain(rpgInit());
- errorChain(uiInit());
#ifdef DUSK_NETWORK
errorChain(networkInit());
#endif
@@ -62,7 +58,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
consolePrint("Assertions real");
#endif
- sceneSet(SCENE_TYPE_INITIAL);
errorOk();
}
@@ -77,10 +72,8 @@ errorret_t engineUpdate(void) {
inputUpdate();
consoleUpdate();
errorChain(audioUpdateEarly());
- errorChain(rpgUpdate());
errorChain(sceneUpdate());
errorChain(assetUpdate());
- errorChain(uiUpdate());
// Render
errorChain(displayUpdate());
@@ -98,9 +91,7 @@ errorret_t engineDispose(void) {
#ifdef DUSK_NETWORK
errorChain(networkDispose());
#endif
- errorChain(rpgDispose());
localeManagerDispose();
- errorChain(uiDispose());
errorChain(audioDispose());
consoleDispose();
errorChain(displayDispose());
diff --git a/src/dusk/locale/localemanager.c b/src/dusk/locale/localemanager.c
index 878a7e53..b71b1990 100644
--- a/src/dusk/locale/localemanager.c
+++ b/src/dusk/locale/localemanager.c
@@ -9,7 +9,6 @@
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
-#include "ui/ui.h"
#include "system/system.h"
#include "console/console.h"
@@ -39,9 +38,6 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
- // TODO : Trigger UI update.
- errorChain(uiUpdateTranslations());
-
errorOk();
}
diff --git a/src/dusk/rpg/CMakeLists.txt b/src/dusk/rpg/CMakeLists.txt
deleted file mode 100644
index 51db8064..00000000
--- a/src/dusk/rpg/CMakeLists.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- rpg.c
- rpgcamera.c
-)
-
-# Subdirs
-add_subdirectory(battle)
-add_subdirectory(cutscene)
-add_subdirectory(entity)
-add_subdirectory(overworld)
-
-add_subdirectory(story)
-add_subdirectory(item)
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/CMakeLists.txt b/src/dusk/rpg/battle/CMakeLists.txt
deleted file mode 100644
index 49363600..00000000
--- a/src/dusk/rpg/battle/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
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- battle.c
- party.c
-)
-
-add_subdirectory(fighter)
-add_subdirectory(move)
-add_subdirectory(state)
diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c
deleted file mode 100644
index f9e2d47a..00000000
--- a/src/dusk/rpg/battle/battle.c
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battle.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "scene/scene.h"
-
-battle_t BATTLE;
-
-void battleInit(void) {
- memoryZero(&BATTLE, sizeof(battle_t));
-}
-
-void battleStart(
- battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
-) {
- assertNotNull(fighters, "Fighters cannot be NULL");
-
- #if DUSK_ASSERTIONS
- // Must be at least two non null fighter present, may need to ensure they
- // are from seperate teams also.
- uint8_t nonNullCount = 0;
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- if(fighters[i] != NULL) nonNullCount++;
- }
- assertTrue(nonNullCount >= 2, "Must have at least two fighters");
- #endif
-
-
- // TODO: I may not need to zero entire battle.
- memoryZero(&BATTLE, sizeof(battle_t));
- memoryCopy(
- BATTLE.fighters,
- fighters,
- sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT
- );
-
- // Enter the opening state.
- battleSetState(BATTLE_STATE_OPENING);
-
- // Change scene
- sceneSet(SCENE_TYPE_BATTLE);
-}
-
-void battleSetState(const battlestate_t next) {
- assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state");
- assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function");
- assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function");
- assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function");
-
- if(BATTLE.state != BATTLE_STATE_NULL) {
- BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
- }
- BATTLE.state = next;
- BATTLE_STATE_FUNCTIONS[BATTLE.state].init();
-}
-
-void battleUpdate(void) {
- if(BATTLE.state == BATTLE_STATE_NULL) return;
- assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
- assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function");
-
- BATTLE_STATE_FUNCTIONS[BATTLE.state].update();
-}
-
-void battleDispose(void) {
- if(BATTLE.state == BATTLE_STATE_NULL) return;
- assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
- assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function");
-
- BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
- memoryZero(&BATTLE, sizeof(battle_t));
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h
deleted file mode 100644
index 108a0742..00000000
--- a/src/dusk/rpg/battle/battle.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/fighter/battlefighter.h"
-#include "rpg/battle/move/battlemove.h"
-#include "rpg/battle/move/battlemoveaction.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-#include "rpg/battle/state/battlestate.h"
-
-typedef enum {
- BATTLE_RESULT_NONE,
- BATTLE_RESULT_WIN,
- BATTLE_RESULT_LOSS,
- BATTLE_RESULT_FLED,
- BATTLE_RESULT_COUNT
-} battleresult_t;
-
-typedef struct {
- battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT];
-
- // Order of who gets to act.
- uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT];
- uint8_t fighterOrderCount;
-
- // Actions to be executed.
- battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT];
- uint8_t actionsPendingCount;
-
- battlestate_t state;
- battlestatedata_t stateData;
-
- // Set once a side has been wiped out (see cutsceneBattlePostMoveStart)
- // or the player has fled (see battleStateSelectionPlayerFlee). Stays
- // BATTLE_RESULT_NONE for the rest of the battle otherwise.
- battleresult_t result;
-} battle_t;
-
-extern battle_t BATTLE;
-
-/**
- * Initializes the battle system. Marks it as inactive with no fighters.
- */
-void battleInit(void);
-
-/**
- * Starts a battle.
- *
- * @param fighters Fighters in the battle.
- */
-void battleStart(
- battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
-);
-
-/**
- * Transitions the battle to a new state: disposes the current state (if
- * any) and initializes next.
- *
- * @param next The state to transition to.
- */
-void battleSetState(const battlestate_t next);
-
-/**
- * Updates the battle system.
- */
-void battleUpdate(void);
-
-/**
- * Disposes of the battle, clearing all fighters and marking it inactive.
- */
-void battleDispose(void);
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/fighter/CMakeLists.txt b/src/dusk/rpg/battle/fighter/CMakeLists.txt
deleted file mode 100644
index bc721bd2..00000000
--- a/src/dusk/rpg/battle/fighter/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- battlefighter.c
-)
diff --git a/src/dusk/rpg/battle/fighter/battlefighter.c b/src/dusk/rpg/battle/fighter/battlefighter.c
deleted file mode 100644
index 27ebe06f..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighter.c
+++ /dev/null
@@ -1,230 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlefighter.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-
-errorret_t battleFighterParseStatField(
- yyjson_val *obj,
- const char_t *key,
- uint16_t *out
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- if(!yyjson_is_int(val)) {
- errorThrow("Fighter: missing/invalid '%s'", key);
- }
-
- const int64_t num = yyjson_get_sint(val);
- if(num < 0 || num > UINT16_MAX) {
- errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX);
- }
-
- *out = (uint16_t)num;
- errorOk();
-}
-
-errorret_t battleFighterParseOptionalIntField(
- yyjson_val *obj,
- const char_t *key,
- const int32_t defaultValue,
- int32_t *out
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- if(val == NULL) {
- *out = defaultValue;
- errorOk();
- }
-
- if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key);
- *out = (int32_t)yyjson_get_sint(val);
- errorOk();
-}
-
-errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) {
- assertNotNull(fighter, "Fighter cannot be NULL");
- assertNotNull(obj, "JSON object cannot be NULL");
-
- int32_t teamNum;
- errorChain(battleFighterParseOptionalIntField(
- obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum
- ));
- if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) {
- errorThrow("Fighter: invalid 'team'");
- }
-
- int32_t controllerNum;
- errorChain(battleFighterParseOptionalIntField(
- obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum
- ));
- if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) {
- errorThrow("Fighter: invalid 'controller'");
- }
-
- battlefighterstats_t stats;
- errorChain(battleFighterParseStatField(obj, "attack", &stats.attack));
- errorChain(battleFighterParseStatField(obj, "defense", &stats.defense));
- errorChain(battleFighterParseStatField(obj, "magic", &stats.magic));
- errorChain(battleFighterParseStatField(obj, "speed", &stats.speed));
- errorChain(battleFighterParseStatField(obj, "luck", &stats.luck));
- errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax));
- errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax));
-
- battleFighterInit(
- fighter, (battlefighterteam_t)teamNum,
- (battlefightercontroller_t)controllerNum, stats
- );
-
- int32_t statusNum;
- errorChain(battleFighterParseOptionalIntField(
- obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum
- ));
- if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) {
- errorThrow("Fighter: invalid 'status'");
- }
- fighter->status = (battlefighterstatus_t)statusNum;
-
- int32_t health;
- errorChain(
- battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health)
- );
- if(health < 0 || health > stats.healthMax) {
- errorThrow("Fighter: 'health' out of range");
- }
- fighter->stats.health = (uint16_t)health;
-
- int32_t mp;
- errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp));
- if(mp < 0 || mp > stats.mpMax) {
- errorThrow("Fighter: 'mp' out of range");
- }
- fighter->stats.mp = (uint16_t)mp;
-
- yyjson_val *abilities = yyjson_obj_get(obj, "abilities");
- if(abilities != NULL) {
- if(
- !yyjson_is_arr(abilities) ||
- yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX
- ) {
- errorThrow(
- "Fighter: 'abilities' must have 0-%u entries",
- BATTLE_FIGHTER_ABILITY_COUNT_MAX
- );
- }
-
- yyjson_arr_iter iter = yyjson_arr_iter_with(abilities);
- yyjson_val *abilityVal;
- while((abilityVal = yyjson_arr_iter_next(&iter)) != NULL) {
- const char_t *name = yyjson_get_str(abilityVal);
- battlemoveid_t ability =
- name == NULL ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
- if(ability == BATTLE_MOVE_ID_NULL) {
- errorThrow("Fighter: invalid entry in 'abilities'");
- }
- battleFighterAbilityAdd(fighter, ability);
- }
- }
-
- errorOk();
-}
-
-yyjson_mut_val *battleFighterSerialize(
- const battlefighter_t *fighter,
- yyjson_mut_doc *doc
-) {
- assertNotNull(fighter, "Fighter cannot be NULL");
- assertNotNull(doc, "Doc cannot be NULL");
-
- yyjson_mut_val *obj = yyjson_mut_obj(doc);
-
- yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team);
- yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller);
- yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status);
-
- yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack);
- yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense);
- yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic);
- yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed);
- yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck);
- yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax);
- yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax);
- yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health);
- yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp);
-
- yyjson_mut_val *abilities = yyjson_mut_arr(doc);
- for(uint8_t i = 0; i < fighter->abilityCount; i++) {
- yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name);
- }
- yyjson_mut_obj_add_val(doc, obj, "abilities", abilities);
-
- return obj;
-}
-
-void battleFighterInit(
- battlefighter_t *fighter,
- const battlefighterteam_t team,
- const battlefightercontroller_t controller,
- const battlefighterstats_t stats
-) {
- assertNotNull(fighter, "Fighter pointer cannot be NULL");
- assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
- assertTrue(
- controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
- "Invalid fighter controller"
- );
-
- const uint8_t id = fighter->id;
- memoryZero(fighter, sizeof(battlefighter_t));
- fighter->id = id;
- fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
- fighter->team = team;
- fighter->controller = controller;
- fighter->stats = stats;
- fighter->stats.health = stats.healthMax;
- fighter->stats.mp = stats.mpMax;
-}
-
-bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
- assertNotNull(fighter, "Fighter pointer cannot be NULL");
- return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
-}
-
-void battleFighterAbilityAdd(
- battlefighter_t *fighter,
- const battlemoveid_t ability
-) {
- assertNotNull(fighter, "Fighter pointer cannot be NULL");
- assertTrue(
- fighter->abilityCount < BATTLE_FIGHTER_ABILITY_COUNT_MAX,
- "Fighter already has the maximum number of abilities"
- );
-
- fighter->abilities[fighter->abilityCount++] = ability;
-}
-
-void battleFighterDamage(
- battlefighter_t *fighter,
- const uint16_t amount
-) {
- assertNotNull(fighter, "Fighter pointer cannot be NULL");
-
- fighter->stats.health =
- amount >= fighter->stats.health ? 0 : fighter->stats.health - amount;
- if(fighter->stats.health == 0) fighter->status = BATTLE_FIGHTER_STATUS_DEAD;
-}
-
-void battleFighterHeal(
- battlefighter_t *fighter,
- const uint16_t amount
-) {
- assertNotNull(fighter, "Fighter pointer cannot be NULL");
- if(!battleFighterIsAlive(fighter)) return;
-
- const uint32_t healed = (uint32_t)fighter->stats.health + amount;
- fighter->stats.health = healed > fighter->stats.healthMax
- ? fighter->stats.healthMax : (uint16_t)healed;
-}
diff --git a/src/dusk/rpg/battle/fighter/battlefighter.h b/src/dusk/rpg/battle/fighter/battlefighter.h
deleted file mode 100644
index 447f9772..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighter.h
+++ /dev/null
@@ -1,127 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "yyjson.h"
-#include "rpg/battle/move/battlemove.h"
-#include "rpg/battle/fighter/battlefighterstatus.h"
-#include "rpg/battle/fighter/battlefighterteam.h"
-#include "rpg/battle/fighter/battlefightercontroller.h"
-#include "rpg/battle/fighter/battlefighterstats.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-
-#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8
-
-typedef struct battlefighter_s {
- uint8_t id;
- battlefighterstatus_t status;
- battlefighterteam_t team;
- battlefightercontroller_t controller;
-
-
- battlefighterstats_t stats;
-
- battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
- battlemoveid_t abilityCount;
-} battlefighter_t;
-
-/**
- * Initializes a battle fighter in place, filling health/mp to the maximum
- * values carried on stats and setting its status to normal.
- *
- * @param fighter Pointer to the fighter to initialize.
- * @param team The team the fighter belongs to.
- * @param controller Who makes decisions for the fighter.
- * @param stats The fighter's base combat stats, including healthMax/mpMax.
- */
-void battleFighterInit(
- battlefighter_t *fighter,
- const battlefighterteam_t team,
- const battlefightercontroller_t controller,
- const battlefighterstats_t stats
-);
-
-/**
- * Generic battlefighter_t parser, shared by every place that builds a
- * fighter from JSON (party.jsonc roster members, BATTLE_START enemy
- * definitions, save-file party members) - see battleFighterSerialize for
- * the inverse. Reads 'team'/'controller'/'status' as ints (each optional,
- * defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a
- * different team/controller, e.g. BATTLE_START's enemies, sets those
- * fields on fighter after this returns), 'attack'/'defense'/'magic'/
- * 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX],
- * 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh
- * fighter), and an optional 'abilities' array of move name strings. Does
- * not touch fighter->id - set it (or leave it) before calling, same as
- * battleFighterInit.
- *
- * @param fighter Pointer to the fighter to fill in.
- * @param obj The JSON object to read fields from.
- * @return Error if a required field is missing/invalid, or an abilities
- * entry doesn't name a real move.
- */
-errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj);
-
-/**
- * Inverse of battleFighterParse: builds a new JSON object (not yet
- * attached anywhere - the caller attaches it, e.g. via
- * yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field
- * battleFighterParse reads except id (see its doc comment for why).
- *
- * @param fighter The fighter to serialize.
- * @param doc The mutable document to allocate the object in.
- * @return The newly built object.
- */
-yyjson_mut_val *battleFighterSerialize(
- const battlefighter_t *fighter,
- yyjson_mut_doc *doc
-);
-
-/**
- * Returns true if the fighter is in a state where it can still act (i.e.
- * is not dead).
- *
- * @param fighter Pointer to the fighter to check.
- * @returns True if the fighter can act.
- */
-bool_t battleFighterIsAlive(const battlefighter_t *fighter);
-
-/**
- * Appends an ability to the fighter's ability list.
- *
- * @param fighter Pointer to the fighter to add the ability to.
- * @param ability The ability to add.
- */
-void battleFighterAbilityAdd(
- battlefighter_t *fighter,
- const battlemoveid_t ability
-);
-
-/**
- * Subtracts amount from the fighter's health (floored at 0), marking it
- * dead once health reaches 0.
- *
- * @param fighter Pointer to the fighter to damage.
- * @param amount The amount of damage to apply.
- */
-void battleFighterDamage(
- battlefighter_t *fighter,
- const uint16_t amount
-);
-
-/**
- * Adds amount to the fighter's health (capped at healthMax). No-op if the
- * fighter isn't alive -- healing can't revive a dead fighter.
- *
- * @param fighter Pointer to the fighter to heal.
- * @param amount The amount to heal.
- */
-void battleFighterHeal(
- battlefighter_t *fighter,
- const uint16_t amount
-);
diff --git a/src/dusk/rpg/battle/fighter/battlefightercontroller.h b/src/dusk/rpg/battle/fighter/battlefightercontroller.h
deleted file mode 100644
index 5e0389d0..00000000
--- a/src/dusk/rpg/battle/fighter/battlefightercontroller.h
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef enum {
- BATTLE_FIGHTER_CONTROLLER_NONE,
-
- BATTLE_FIGHTER_CONTROLLER_PLAYER,
- BATTLE_FIGHTER_CONTROLLER_AI,
-
- BATTLE_FIGHTER_CONTROLLER_COUNT
-} battlefightercontroller_t;
diff --git a/src/dusk/rpg/battle/fighter/battlefighterpos.h b/src/dusk/rpg/battle/fighter/battlefighterpos.h
deleted file mode 100644
index e7f237b0..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighterpos.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef uint8_t battlefighterpos_t;
-
-#define BATTLE_FIGHTER_POS(x) ((battlefighterpos_t)(x))
-
-// Left (Enemy side)
-#define BATTLE_FIGHTER_POS_0 BATTLE_FIGHTER_POS(0)
-#define BATTLE_FIGHTER_POS_1 BATTLE_FIGHTER_POS(1)
-#define BATTLE_FIGHTER_POS_2 BATTLE_FIGHTER_POS(2)
-#define BATTLE_FIGHTER_POS_3 BATTLE_FIGHTER_POS(3)
-
-// Right (Ally side)
-#define BATTLE_FIGHTER_POS_4 BATTLE_FIGHTER_POS(4)
-#define BATTLE_FIGHTER_POS_5 BATTLE_FIGHTER_POS(5)
-#define BATTLE_FIGHTER_POS_6 BATTLE_FIGHTER_POS(6)
-#define BATTLE_FIGHTER_POS_7 BATTLE_FIGHTER_POS(7)
-
-#define BATTLE_FIGHTER_POS_COUNT (BATTLE_FIGHTER_POS_7 + 1)
-
-// Sentinels resolved by battleStateExecutingInit against the currently
-// resolving action, for cutscene items authored inside a move's own
-// cutscene (e.g. cutscenes/battle/moves/slash.jsonc) that need to refer
-// to "whoever is being hit"/"whoever is attacking" without knowing their
-// concrete BATTLE.fighters index ahead of time.
-#define BATTLE_FIGHTER_POS_TARGET BATTLE_FIGHTER_POS(0xFE)
-#define BATTLE_FIGHTER_POS_USER BATTLE_FIGHTER_POS(0xFD)
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/fighter/battlefighterspecies.h b/src/dusk/rpg/battle/fighter/battlefighterspecies.h
deleted file mode 100644
index 92162b98..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighterspecies.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/battle/fighter/battlefighterstats.h"
-
-#define BATTLE_FIGHTER_SPECIES_NAME_MAX 16
-#define BATTLE_FIGHTER_SPECIES_COUNT_MAX 250
-
-typedef uint8_t battlespeciesid_t;
-
-#define BATTLE_SPECIES_ID_NULL ((battlespeciesid_t)0)
-
-typedef struct {
- char_t name[BATTLE_FIGHTER_SPECIES_NAME_MAX];
- battlefighterstats_t baseStats;
-} battlefighterspecies_t;
diff --git a/src/dusk/rpg/battle/fighter/battlefighterstats.h b/src/dusk/rpg/battle/fighter/battlefighterstats.h
deleted file mode 100644
index f4b731bb..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighterstats.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
- uint16_t health;
- uint16_t mp;
-
- uint16_t healthMax;
- uint16_t mpMax;
- uint16_t attack;
- uint16_t defense;
- uint16_t magic;
- uint16_t speed;
- uint16_t luck;
-} battlefighterstats_t;
diff --git a/src/dusk/rpg/battle/fighter/battlefighterstatus.h b/src/dusk/rpg/battle/fighter/battlefighterstatus.h
deleted file mode 100644
index 3c73af2d..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighterstatus.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef enum {
- BATTLE_FIGHTER_STATUS_NULL,
-
- BATTLE_FIGHTER_STATUS_NORMAL,
- BATTLE_FIGHTER_STATUS_DEAD,
-
- // Poisoned, burned, confused, etc.
-
- BATTLE_FIGHTER_STATUS_COUNT
-} battlefighterstatus_t;
diff --git a/src/dusk/rpg/battle/fighter/battlefighterteam.h b/src/dusk/rpg/battle/fighter/battlefighterteam.h
deleted file mode 100644
index 85abda07..00000000
--- a/src/dusk/rpg/battle/fighter/battlefighterteam.h
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef enum {
- BATTLE_FIGHTER_TEAM_ALLY,
- BATTLE_FIGHTER_TEAM_ENEMY,
-
- BATTLE_FIGHTER_TEAM_COUNT
-} battlefighterteam_t;
diff --git a/src/dusk/rpg/battle/move/CMakeLists.txt b/src/dusk/rpg/battle/move/CMakeLists.txt
deleted file mode 100644
index cdbe40d2..00000000
--- a/src/dusk/rpg/battle/move/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- battlemove.c
-)
diff --git a/src/dusk/rpg/battle/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c
deleted file mode 100644
index 389c5ee0..00000000
--- a/src/dusk/rpg/battle/move/battlemove.c
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlemove.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include
-
-battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
-uint8_t BATTLE_MOVE_COUNT;
-
-errorret_t battleMoveInit(void) {
- memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES));
- BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL.
-
- // Get moves JSON.
- assetentry_t *entry = assetLock(
- "battle/moves.jsonc", ASSET_LOADER_TYPE_JSON, NULL
- );
- errorret_t ret = assetRequireLoaded(entry);
- if(errorIsNotOk(ret)) {
- assetUnlockEntry(entry);
- errorChain(ret);
- }
-
- yyjson_val *root = yyjson_doc_get_root(entry->data.json);
- yyjson_val *moves = yyjson_obj_get(root, "moves");
- yyjson_obj_iter iter = yyjson_obj_iter_with(moves);
- yyjson_val *key;
- while((key = yyjson_obj_iter_next(&iter))) {
- if(BATTLE_MOVE_COUNT >= BATTLE_MOVE_COUNT_MAX) {
- assetUnlockEntry(entry);
- errorThrow(
- "Battle move: too many moves defined (max %u)",
- (uint32_t)BATTLE_MOVE_COUNT_MAX
- );
- }
-
- battlemoveid_t id = BATTLE_MOVE_COUNT++;
- battleability_t *move = &BATTLE_MOVES[id];
-
- // Name
- if(!yyjson_is_str(key)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move: keys in 'moves' must be strings");
- }
- const char_t *name = yyjson_get_str(key);
- stringCopy(move->name, name, BATTLE_MOVE_NAME_MAX);
-
- if(strlen(name) >= BATTLE_MOVE_NAME_MAX) {
- assetUnlockEntry(entry);
- errorThrow(
- "Battle move '%s': name must be less than %u characters",
- name, (uint32_t)BATTLE_MOVE_NAME_MAX
- );
- }
-
- // Must me object
- yyjson_val *val = yyjson_obj_iter_get_val(key);
- if(!yyjson_is_obj(val)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': value must be an object", name);
- }
-
- // Valid targets
- yyjson_val *enemies = yyjson_obj_get(val, "enemies");
- if(enemies != NULL && !yyjson_is_bool(enemies)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': 'enemies' must be a boolean", name);
- }
- yyjson_val *allies = yyjson_obj_get(val, "allies");
- if(allies != NULL && !yyjson_is_bool(allies)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': 'allies' must be a boolean", name);
- }
- yyjson_val *multiple = yyjson_obj_get(val, "multiple");
- if(multiple != NULL && !yyjson_is_bool(multiple)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': 'multiple' must be a boolean", name);
- }
- yyjson_val *self = yyjson_obj_get(val, "self");
- if(self != NULL && !yyjson_is_bool(self)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': 'self' must be a boolean", name);
- }
-
- battletarget_t target = 0;
- if(yyjson_get_bool(enemies)) target |= BATTLE_TARGET_ENEMIES;
- if(yyjson_get_bool(allies)) target |= BATTLE_TARGET_ALLIES;
- if(yyjson_get_bool(multiple)) target |= BATTLE_TARGET_MULTIPLE;
- if(yyjson_get_bool(self)) target |= BATTLE_TARGET_SELF;
- move->target = target;
- if(move->target == 0) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': missing target type", name);
- }
-
- // Default target (optional)
- yyjson_val *defaultVal = yyjson_obj_get(val, "default");
- if(defaultVal != NULL && !yyjson_is_str(defaultVal)) {
- assetUnlockEntry(entry);
- errorThrow("Battle move '%s': 'default' must be a string", name);
- }
-
- const char_t *defaultTarget = yyjson_get_str(defaultVal);
- if(defaultTarget == NULL) {
- move->targetDefault = 0;
- } else if(stringCompareInsensitive(defaultTarget, "enemy") == 0) {
- move->targetDefault = BATTLE_TARGET_ENEMIES;
- } else if(stringCompareInsensitive(defaultTarget, "ally") == 0) {
- move->targetDefault = BATTLE_TARGET_ALLIES;
- } else if(stringCompareInsensitive(defaultTarget, "self") == 0) {
- move->targetDefault = BATTLE_TARGET_SELF;
- } else {
- assetUnlockEntry(entry);
- errorThrow("Battle move %s has invalid default target.", name);
- }
- }
-
- assetUnlockEntry(entry);
- errorOk();
-}
-
-battlemoveid_t battleMoveGetIdByName(const char_t *name) {
- assertNotNull(name, "name must not be NULL");
-
- for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) {
- if(stringEquals(BATTLE_MOVES[id].name, name)) return id;
- }
-
- return BATTLE_MOVE_ID_NULL;
-}
diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h
deleted file mode 100644
index 3c11b2d2..00000000
--- a/src/dusk/rpg/battle/move/battlemove.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "asset/asset.h"
-#include "battletarget.h"
-
-typedef uint8_t battlemoveid_t;
-
-#define BATTLE_MOVE_NAME_MAX 16
-#define BATTLE_MOVE_COUNT_MAX 250
-
-#define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0)
-
-extern uint8_t BATTLE_MOVE_COUNT;
-
-typedef struct {
- char_t name[BATTLE_MOVE_NAME_MAX];
- battletarget_t target;
- battletarget_t targetDefault;
-} battleability_t;
-
-extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
-
-/**
- * Initializes the battle move subsystem, loading assets/battle/moves.jsonc.
- *
- * @return Any error that occurs.
- */
-errorret_t battleMoveInit(void);
-
-/**
- * Finds a move's id by name (exact match).
- *
- * @param name The move name to search for.
- * @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found.
- */
-battlemoveid_t battleMoveGetIdByName(const char_t *name);
diff --git a/src/dusk/rpg/battle/move/battlemoveaction.h b/src/dusk/rpg/battle/move/battlemoveaction.h
deleted file mode 100644
index f39f8852..00000000
--- a/src/dusk/rpg/battle/move/battlemoveaction.h
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/move/battlemove.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-
-typedef struct battlefighter_s battlefighter_t;
-
-typedef struct {
- battlemoveid_t move;
- battlefighterpos_t targets[BATTLE_FIGHTER_POS_COUNT];
- battlefighter_t *user;
-} battlemoveaction_t;
diff --git a/src/dusk/rpg/battle/move/battletarget.h b/src/dusk/rpg/battle/move/battletarget.h
deleted file mode 100644
index 73abfb38..00000000
--- a/src/dusk/rpg/battle/move/battletarget.h
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef uint8_t battletarget_t;
-
-#define BATTLE_TARGET_ENEMIES (1 << 0)
-#define BATTLE_TARGET_ALLIES (1 << 1)
-#define BATTLE_TARGET_MULTIPLE (1 << 2)
-#define BATTLE_TARGET_SELF (1 << 3)
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/party.c b/src/dusk/rpg/battle/party.c
deleted file mode 100644
index 016e4ca3..00000000
--- a/src/dusk/rpg/battle/party.c
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "party.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "save/save.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-
-errorret_t partyInit(party_t *party) {
- assertNotNull(party, "Party cannot be NULL");
-
- memoryZero(party, sizeof(party_t));
- for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
- party->order[i] = PARTY_ORDER_EMPTY;
- }
-
- assetentry_t *entry = assetLock("party.jsonc", ASSET_LOADER_TYPE_JSON, NULL);
- errorret_t result = assetRequireLoaded(entry);
- if(errorIsNotOk(result)) {
- assetUnlockEntry(entry);
- errorChain(result);
- }
-
- yyjson_val *root = yyjson_doc_get_root(entry->data.json);
- yyjson_val *members = yyjson_obj_get(root, "members");
- if(!yyjson_is_obj(members)) {
- assetUnlockEntry(entry);
- errorThrow("party.jsonc: missing/invalid 'members' object");
- }
-
- const size_t memberCount = yyjson_obj_size(members);
- if(memberCount > PARTY_MEMBER_COUNT_MAX) {
- assetUnlockEntry(entry);
- errorThrow(
- "party.jsonc: defines more than PARTY_MEMBER_COUNT_MAX (%u) members",
- PARTY_MEMBER_COUNT_MAX
- );
- }
-
- uint8_t idx = 0;
- yyjson_val *key;
- yyjson_val *memberObj;
- yyjson_obj_iter iter = yyjson_obj_iter_with(members);
- while((key = yyjson_obj_iter_next(&iter))) {
- memberObj = yyjson_obj_iter_get_val(key);
-
- if(yyjson_get_len(key) >= PARTY_KEY_LENGTH_MAX) {
- assetUnlockEntry(entry);
- errorThrow(
- "party.jsonc: member key '%s' exceeds PARTY_KEY_LENGTH_MAX (%u)",
- yyjson_get_str(key), PARTY_KEY_LENGTH_MAX
- );
- }
-
- stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
-
- battlefighter_t *member = &party->members[idx];
- member->id = idx;
- errorret_t parseResult = battleFighterParse(member, memberObj);
- if(errorIsNotOk(parseResult)) {
- assetUnlockEntry(entry);
- errorChain(parseResult);
- }
-
- idx++;
- }
-
- assetUnlockEntry(entry);
-
- // TEST: only the first defined member starts active, in the first party
- // position - replace once there's a real starting-party/character-
- // creation flow. Deliberately separate from the roster seeding above:
- // order (not a member's presence in the roster) is what determines who's
- // currently in the party.
- if(memberCount > 0) party->order[0] = 0;
-
- errorOk();
-}
-
-uint8_t partyGetMemberIndexByKey(const char_t *key) {
- assertNotNull(key, "Key cannot be NULL");
-
- const party_t *party = &SAVE.slot.party;
- for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
- if(stringEquals(party->keys[i], key)) return i;
- }
-
- return PARTY_MEMBER_NOT_FOUND;
-}
-
-const char_t *partyGetMemberKey(const uint8_t memberIndex) {
- assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
- return SAVE.slot.party.keys[memberIndex];
-}
-
-bool_t partyIsMemberActive(const uint8_t memberIndex) {
- assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
-
- const party_t *party = &SAVE.slot.party;
- for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
- if(party->order[i] == memberIndex) return true;
- }
-
- return false;
-}
-
-void partyAddMember(const uint8_t memberIndex) {
- assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
-
- party_t *party = &SAVE.slot.party;
- assertTrue(
- party->members[memberIndex].status != BATTLE_FIGHTER_STATUS_NULL,
- "Member does not exist in the roster"
- );
-
- if(partyIsMemberActive(memberIndex)) return;
-
- for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
- if(party->order[i] != PARTY_ORDER_EMPTY) continue;
- party->order[i] = memberIndex;
- return;
- }
-}
-
-battlefighter_t *partyGetOrderMember(const uint8_t slot) {
- assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
-
- party_t *party = &SAVE.slot.party;
- const uint8_t index = party->order[slot];
- if(index == PARTY_ORDER_EMPTY) return NULL;
- return &party->members[index];
-}
-
-void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
- assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
- assertTrue(
- memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
- "Invalid party member index"
- );
-
- SAVE.slot.party.order[slot] = memberIndex;
-}
-
-void partyRemoveMember(const uint8_t memberIndex) {
- assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
-
- party_t *party = &SAVE.slot.party;
-
- uint8_t removedSlot = PARTY_ORDER_EMPTY;
- for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
- if(party->order[i] != memberIndex) continue;
- removedSlot = i;
- break;
- }
- if(removedSlot == PARTY_ORDER_EMPTY) return;
-
- for(uint8_t i = removedSlot; i < PARTY_ACTIVE_SIZE_MAX - 1; i++) {
- party->order[i] = party->order[i + 1];
- }
- party->order[PARTY_ACTIVE_SIZE_MAX - 1] = PARTY_ORDER_EMPTY;
-}
-
-void partyHealMember(const uint8_t memberIndex) {
- assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
-
- battlefighter_t *member = &SAVE.slot.party.members[memberIndex];
- if(member->status == BATTLE_FIGHTER_STATUS_NULL) return;
-
- member->status = BATTLE_FIGHTER_STATUS_NORMAL;
- member->stats.health = member->stats.healthMax;
- member->stats.mp = member->stats.mpMax;
-}
-
-void partyHealAll(void) {
- const party_t *party = &SAVE.slot.party;
-
- for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
- if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
- partyHealMember(i);
- }
-}
diff --git a/src/dusk/rpg/battle/party.h b/src/dusk/rpg/battle/party.h
deleted file mode 100644
index 8feedaef..00000000
--- a/src/dusk/rpg/battle/party.h
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/fighter/battlefighter.h"
-#include "yyjson.h"
-
-#define PARTY_MEMBER_COUNT_MAX 10
-#define PARTY_ACTIVE_SIZE_MAX 4
-#define PARTY_KEY_LENGTH_MAX 32
-#define PARTY_ORDER_EMPTY 0xFF
-#define PARTY_MEMBER_NOT_FOUND 0xFF
-
-typedef struct {
- battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
- char_t keys[PARTY_MEMBER_COUNT_MAX][PARTY_KEY_LENGTH_MAX];
- uint8_t order[PARTY_ACTIVE_SIZE_MAX];
-} party_t;
-
-/**
- * Initializes party with an empty order, then seeds every roster slot
- * directly from party.jsonc's "members" object (each key/value pair fills
- * the next members[]/keys[] slot in the object's iteration order, key
- * copied into keys[] and value parsed via battleFighterParse) - unlike
- * partyAddMember, this never touches order itself except for one
- * placeholder default: if party.jsonc defines at least one member,
- * order[0] is set to 0 (the first member starts active in the first party
- * position). Replace this default once there's a real starting-party/
- * character-creation flow.
- *
- * @param party The party to initialize.
- * @return Error if party.jsonc fails to load/parse, defines more members
- * than PARTY_MEMBER_COUNT_MAX has room for, or a key exceeds
- * PARTY_KEY_LENGTH_MAX.
- */
-errorret_t partyInit(party_t *party);
-
-/**
- * Looks up a roster member's index by its party.jsonc key (see
- * party_t.keys).
- *
- * @param key The member's key to search for.
- * @return The matching roster index, or PARTY_MEMBER_NOT_FOUND if no
- * member has that key.
- */
-uint8_t partyGetMemberIndexByKey(const char_t *key);
-
-/**
- * Gets a roster member's party.jsonc key by index (see party_t.keys).
- *
- * @param memberIndex The roster member index to query.
- * @return The member's key - "" if memberIndex is past however many
- * members party.jsonc actually defines.
- */
-const char_t *partyGetMemberKey(const uint8_t memberIndex);
-
-/**
- * Checks whether memberIndex currently occupies a slot in the current save
- * slot's party order (i.e. is actively in the party right now).
- *
- * @param memberIndex The roster member index to check.
- * @return True if memberIndex appears anywhere in order.
- */
-bool_t partyIsMemberActive(const uint8_t memberIndex);
-
-/**
- * Adds an existing roster member (see partyInit - every roster slot
- * always exists once initialized) to the current save slot's active party
- * order, in the first free order slot. No-op if memberIndex is already
- * active, or if there is no free order slot.
- *
- * @param memberIndex The roster member index to activate.
- */
-void partyAddMember(const uint8_t memberIndex);
-
-/**
- * Gets the roster member currently occupying an active order slot in the
- * current save slot's party.
- *
- * @param slot The active order slot to query.
- * @return Pointer to the member in that slot, or NULL if the slot is
- * empty.
- */
-battlefighter_t *partyGetOrderMember(const uint8_t slot);
-
-/**
- * Assigns a roster member to an active order slot in the current save
- * slot's party, replacing whatever was there. Use PARTY_ORDER_EMPTY to
- * clear a slot.
- *
- * @param slot The active order slot to assign.
- * @param memberIndex The roster member index to place there, or
- * PARTY_ORDER_EMPTY to clear the slot.
- */
-void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
-
-/**
- * Removes memberIndex from the current save slot's active party order -
- * the roster member itself is untouched (it still exists, per partyInit;
- * it's simply no longer active) - shifting every later order entry down
- * one slot to close the gap, so order stays packed with no holes between
- * active members. No-op if memberIndex isn't currently active.
- *
- * @param memberIndex The roster member index to deactivate.
- */
-void partyRemoveMember(const uint8_t memberIndex);
-
-/**
- * Fully restores a roster member's health/mp to their max and clears a
- * dead status back to normal - unlike battleFighterHeal, this can revive
- * a dead member (there's no combat context here to keep them down). No-op
- * if the slot is empty.
- *
- * @param memberIndex The roster member index to heal.
- */
-void partyHealMember(const uint8_t memberIndex);
-
-/**
- * Calls partyHealMember for every occupied slot in the current save
- * slot's party roster.
- */
-void partyHealAll(void);
diff --git a/src/dusk/rpg/battle/state/CMakeLists.txt b/src/dusk/rpg/battle/state/CMakeLists.txt
deleted file mode 100644
index 603227d0..00000000
--- a/src/dusk/rpg/battle/state/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- battlestate.c
- battlestateopening.c
- battlestatepreround.c
- battlestateselection.c
- battlestateexecuting.c
- battlestatepostround.c
- battlestatelost.c
- battlestatewon.c
- battlestatefled.c
- battlestateending.c
-)
diff --git a/src/dusk/rpg/battle/state/battlestate.c b/src/dusk/rpg/battle/state/battlestate.c
deleted file mode 100644
index d5387658..00000000
--- a/src/dusk/rpg/battle/state/battlestate.c
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestate.h"
-
-battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT] = {
- [BATTLE_STATE_NULL] = { 0 },
-
- [BATTLE_STATE_OPENING] = {
- .init = battleStateOpeningInit,
- .update = battleStateOpeningUpdate,
- .dispose = battleStateOpeningDispose
- },
-
- [BATTLE_STATE_PRE_ROUND] = {
- .init = battleStatePreRoundInit,
- .update = battleStatePreRoundUpdate,
- .dispose = battleStatePreRoundDispose
- },
-
- [BATTLE_STATE_SELECTION] = {
- .init = battleStateSelectionInit,
- .update = battleStateSelectionUpdate,
- .dispose = battleStateSelectionDispose
- },
-
- [BATTLE_STATE_EXECUTING] = {
- .init = battleStateExecutingInit,
- .update = battleStateExecutingUpdate,
- .dispose = battleStateExecutingDispose
- },
-
- [BATTLE_STATE_POST_ROUND] = {
- .init = battleStatePostRoundInit,
- .update = battleStatePostRoundUpdate,
- .dispose = battleStatePostRoundDispose
- },
-
- [BATTLE_STATE_LOST] = {
- .init = battleStateLostInit,
- .update = battleStateLostUpdate,
- .dispose = battleStateLostDispose
- },
-
- [BATTLE_STATE_WON] = {
- .init = battleStateWonInit,
- .update = battleStateWonUpdate,
- .dispose = battleStateWonDispose
- },
-
- [BATTLE_STATE_FLED] = {
- .init = battleStateFledInit,
- .update = battleStateFledUpdate,
- .dispose = battleStateFledDispose
- },
-
- [BATTLE_STATE_ENDING] = {
- .init = battleStateEndingInit,
- .update = battleStateEndingUpdate,
- .dispose = battleStateEndingDispose
- }
-};
diff --git a/src/dusk/rpg/battle/state/battlestate.h b/src/dusk/rpg/battle/state/battlestate.h
deleted file mode 100644
index 00eddd5e..00000000
--- a/src/dusk/rpg/battle/state/battlestate.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "battlestateopening.h"
-#include "battlestatepreround.h"
-#include "battlestateselection.h"
-#include "battlestateexecuting.h"
-#include "battlestatepostround.h"
-#include "battlestatelost.h"
-#include "battlestatewon.h"
-#include "battlestatefled.h"
-#include "battlestateending.h"
-
-typedef enum {
- // No active battle.
- BATTLE_STATE_NULL,
-
- // Battle hasn't actually started yet, equiv to trainer intro in Pokemon.
- BATTLE_STATE_OPENING,
- // Pre round, anything that happens before AI or Player can act.
- BATTLE_STATE_PRE_ROUND,
- // Player is deciding what to do, waiting for their input
- BATTLE_STATE_SELECTION,
- // Currently executing the moves.
- BATTLE_STATE_EXECUTING,
- // Post round, anything that happens after AI or Player has acted, loops back
- // after this point
- BATTLE_STATE_POST_ROUND,
- // Battle has ended with a player loss
- BATTLE_STATE_LOST,
- // Battle has ended with a player win
- BATTLE_STATE_WON,
- // Battle has ended with a player fleeing
- BATTLE_STATE_FLED,
- // Battle state is done, this is fade out or whatever.
- BATTLE_STATE_ENDING,
-
- BATTLE_STATE_COUNT
-} battlestate_t;
-
-typedef union {
- battlestateopening_t opening;
- battlestatepreround_t preRound;
- battlestateselection_t selection;
- battlestateexecuting_t executing;
- battlestatepostround_t postRound;
- battlestatelost_t lost;
- battlestatewon_t won;
- battlestatefled_t fled;
- battlestateending_t ending;
-} battlestatedata_t;
-
-typedef struct {
- void (*init)();
- void (*update)();
- void (*dispose)();
-} battlestatefunctions_t;
-
-extern battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT];
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/state/battlestateending.c b/src/dusk/rpg/battle/state/battlestateending.c
deleted file mode 100644
index 2a195cbc..00000000
--- a/src/dusk/rpg/battle/state/battlestateending.c
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestateending.h"
-#include "rpg/battle/battle.h"
-#include "scene/scene.h"
-
-void battleStateEndingInit(void) {
- // Tear down the battle and hand control back to the overworld. The
- // overworld scene keeps no state of its own in scenedata_t (player/map
- // state lives in the separate global entity/map systems, untouched by
- // switching to/from SCENE_TYPE_BATTLE), so there's nothing else to
- // restore here.
- battleDispose();
- sceneSet(SCENE_TYPE_OVERWORLD);
-}
-
-void battleStateEndingUpdate(void) {
-
-}
-
-void battleStateEndingDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestateending.h b/src/dusk/rpg/battle/state/battlestateending.h
deleted file mode 100644
index 7b4fc581..00000000
--- a/src/dusk/rpg/battle/state/battlestateending.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestateending_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle
- * (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD.
- */
-void battleStateEndingInit(void);
-
-/**
- * Updates BATTLE_STATE_ENDING for one frame.
- */
-void battleStateEndingUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_ENDING.
- */
-void battleStateEndingDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c
deleted file mode 100644
index d0045a38..00000000
--- a/src/dusk/rpg/battle/state/battlestateexecuting.c
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestateexecuting.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/cutscene.h"
-#include "asset/assetfile.h"
-#include "ui/overlay/uifatalerror.h"
-#include "assert/assert.h"
-#include "util/string.h"
-
-
-void battleStateExecutingResolveFighterIndex(
- battlefighterpos_t *fighterIndex,
- const uint8_t userIndex,
- const battlemoveaction_t *action
-) {
- if(*fighterIndex == BATTLE_FIGHTER_POS_TARGET) {
- *fighterIndex = action->targets[0];
- } else if(*fighterIndex == BATTLE_FIGHTER_POS_USER) {
- *fighterIndex = userIndex;
- }
-}
-
-void battleStateExecutingInit(void) {
- cutsceneitem_t items[BATTLE_STATE_EXECUTING_ITEMS_MAX];
- uint8_t itemsCount = 0;
-
- // For each action (just queued during selection). actionsPending is
- // indexed by fighter position, not sequentially, so scan every position
- // and stop early once every pending action has been processed.
- uint8_t processed = 0;
- for(
- uint8_t i = 0;
- i < BATTLE_FIGHTER_POS_COUNT && processed < BATTLE.actionsPendingCount;
- i++
- ) {
- // Get action.
- battlemoveaction_t *action = &BATTLE.actionsPending[i];
- if(action->move == BATTLE_MOVE_ID_NULL) continue;
- processed++;
-
- assertTrue(action->move < BATTLE_MOVE_COUNT, "Invalid move id");
-
- // Get ability.
- battleability_t *ability = &BATTLE_MOVES[action->move];
- assertStrLenMin(ability->name, 1, "Invalid move name");
-
- // Determine cutscene path
- char_t path[ASSET_FILE_NAME_MAX];
- stringFormat(
- path, ASSET_FILE_NAME_MAX - 1,
- "cutscenes/battle/moves/%s.jsonc", ability->name
- );
-
- // Load and parse the move's cutscene straight into the tail of items.
- const uint8_t moveItemsStart = itemsCount;
- cutscene_t scene;
- errorret_t result = cutsceneLoadParse(
- path, &scene, &items[itemsCount],
- BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
- );
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(NULL);
- continue;
- }
- itemsCount += scene.itemCount;
-
- // Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
- // action, now that its user/target fighters are known.
- for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
- cutsceneitem_t *moveItem = &items[j];
- if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE) {
- battleStateExecutingResolveFighterIndex(
- &moveItem->battleFighterDamage.fighterIndex, i, action
- );
- } else if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL) {
- battleStateExecutingResolveFighterIndex(
- &moveItem->battleFighterHeal.fighterIndex, i, action
- );
- }
- }
-
- // Insert end of attack cutscene item.
- items[itemsCount++] = (cutsceneitem_t){
- .type = CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE
- };
-
- // This action has now been resolved - clear it so it isn't
- // re-executed (and doesn't block re-selection) next round.
- action->move = BATTLE_MOVE_ID_NULL;
- }
- BATTLE.actionsPendingCount = 0;
-
- // After all moves completed, transition to pre-round for next round.
- items[itemsCount++] = (cutsceneitem_t){
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
- };
- cutsceneSystemInsertItemsNext(items, itemsCount);
-}
-
-void battleStateExecutingUpdate(void) {
-
-}
-
-void battleStateExecutingDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h
deleted file mode 100644
index 8e51d3e9..00000000
--- a/src/dusk/rpg/battle/state/battlestateexecuting.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-#include "rpg/battle/move/battlemoveaction.h"
-
-typedef struct {
-
-} battlestateexecuting_t;
-
-#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
-
-/**
- * Called when the battle enters BATTLE_STATE_EXECUTING.
- */
-void battleStateExecutingInit(void);
-
-/**
- * Resolves a BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER sentinel
- * (as parsed onto a BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL item inside
- * a move's own cutscene) against the action currently being resolved
- * into cutscene items. Leaves fighterIndex untouched if it's already a
- * concrete BATTLE.fighters index.
- *
- * @param fighterIndex The field to resolve in place.
- * @param userIndex Index into BATTLE.fighters of the fighter performing
- * the action.
- * @param action The action being resolved.
- */
-void battleStateExecutingResolveFighterIndex(
- battlefighterpos_t *fighterIndex,
- const uint8_t userIndex,
- const battlemoveaction_t *action
-);
-
-/**
- * Updates BATTLE_STATE_EXECUTING for one frame.
- */
-void battleStateExecutingUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_EXECUTING.
- */
-void battleStateExecutingDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestatefled.c b/src/dusk/rpg/battle/state/battlestatefled.c
deleted file mode 100644
index 441072a2..00000000
--- a/src/dusk/rpg/battle/state/battlestatefled.c
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestatefled.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void battleStateFledInit(void) {
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_TEXT,
- .text = { .text = "Got away safely!" }
- },
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_ENDING }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-void battleStateFledUpdate(void) {
-
-}
-
-void battleStateFledDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestatefled.h b/src/dusk/rpg/battle/state/battlestatefled.h
deleted file mode 100644
index f482b6be..00000000
--- a/src/dusk/rpg/battle/state/battlestatefled.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestatefled_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_FLED. Queues a textbox
- * announcing the escape, then transitions to BATTLE_STATE_ENDING once
- * dismissed.
- */
-void battleStateFledInit(void);
-
-/**
- * Updates BATTLE_STATE_FLED for one frame.
- */
-void battleStateFledUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_FLED.
- */
-void battleStateFledDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestatelost.c b/src/dusk/rpg/battle/state/battlestatelost.c
deleted file mode 100644
index 334d8d87..00000000
--- a/src/dusk/rpg/battle/state/battlestatelost.c
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestatelost.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void battleStateLostInit(void) {
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_TEXT,
- .text = { .text = "You were defeated..." }
- },
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_ENDING }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-void battleStateLostUpdate(void) {
-
-}
-
-void battleStateLostDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestatelost.h b/src/dusk/rpg/battle/state/battlestatelost.h
deleted file mode 100644
index 7a20dca7..00000000
--- a/src/dusk/rpg/battle/state/battlestatelost.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestatelost_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_LOST. Queues a textbox
- * announcing the loss, then transitions to BATTLE_STATE_ENDING once
- * dismissed.
- */
-void battleStateLostInit(void);
-
-/**
- * Updates BATTLE_STATE_LOST for one frame.
- */
-void battleStateLostUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_LOST.
- */
-void battleStateLostDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestateopening.c b/src/dusk/rpg/battle/state/battlestateopening.c
deleted file mode 100644
index eb2a2752..00000000
--- a/src/dusk/rpg/battle/state/battlestateopening.c
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestateopening.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void battleStateOpeningInit(void) {
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-void battleStateOpeningUpdate(void) {
-
-}
-
-void battleStateOpeningDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestateopening.h b/src/dusk/rpg/battle/state/battlestateopening.h
deleted file mode 100644
index 946c4d06..00000000
--- a/src/dusk/rpg/battle/state/battlestateopening.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestateopening_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_OPENING.
- */
-void battleStateOpeningInit(void);
-
-/**
- * Updates BATTLE_STATE_OPENING for one frame.
- */
-void battleStateOpeningUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_OPENING.
- */
-void battleStateOpeningDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestatepostround.c b/src/dusk/rpg/battle/state/battlestatepostround.c
deleted file mode 100644
index 2e713610..00000000
--- a/src/dusk/rpg/battle/state/battlestatepostround.c
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestatepostround.h"
-
-void battleStatePostRoundInit(void) {
-
-}
-
-void battleStatePostRoundUpdate(void) {
-
-}
-
-void battleStatePostRoundDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestatepostround.h b/src/dusk/rpg/battle/state/battlestatepostround.h
deleted file mode 100644
index cba3a38f..00000000
--- a/src/dusk/rpg/battle/state/battlestatepostround.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestatepostround_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_POST_ROUND.
- */
-void battleStatePostRoundInit(void);
-
-/**
- * Updates BATTLE_STATE_POST_ROUND for one frame.
- */
-void battleStatePostRoundUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_POST_ROUND.
- */
-void battleStatePostRoundDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c
deleted file mode 100644
index 7b85ece7..00000000
--- a/src/dusk/rpg/battle/state/battlestatepreround.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestatepreround.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void battleStatePreRoundInit(void) {
- // TODO: Determine selection order and who gets to act.
- BATTLE.fighterOrderCount = 0;
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- if(BATTLE.fighters[i] == NULL) continue;
- if(!battleFighterIsAlive(BATTLE.fighters[i])) continue;
- BATTLE.fighterOrder[BATTLE.fighterOrderCount++] = i;
- }
-
- // Queue up the set state and wait for selection to stop cutscene ending.
- cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_SELECTION }
- },
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-void battleStatePreRoundUpdate(void) {
-
-}
-
-void battleStatePreRoundDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestatepreround.h b/src/dusk/rpg/battle/state/battlestatepreround.h
deleted file mode 100644
index 0f26afe9..00000000
--- a/src/dusk/rpg/battle/state/battlestatepreround.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestatepreround_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_PRE_ROUND.
- */
-void battleStatePreRoundInit(void);
-
-/**
- * Updates BATTLE_STATE_PRE_ROUND for one frame.
- */
-void battleStatePreRoundUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_PRE_ROUND.
- */
-void battleStatePreRoundDispose(void);
diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c
deleted file mode 100644
index 8bd2e2f8..00000000
--- a/src/dusk/rpg/battle/state/battlestateselection.c
+++ /dev/null
@@ -1,146 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestateselection.h"
-#include "rpg/battle/battle.h"
-#include "assert/assert.h"
-
-void battleStateSelectionInit(void) {
- BATTLE.stateData.selection.selectionIndex = 0;
-}
-
-bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex) {
- assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
- return BATTLE.actionsPending[fighterIndex].move != BATTLE_MOVE_ID_NULL;
-}
-
-bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) {
- assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
-
- battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- if(fighter == NULL) return false;
- if(!battleFighterIsAlive(fighter)) return false;
-
- return !battleStateSelectionFighterHasDecided(fighterIndex);
-}
-
-bool_t battleStateSelectionIsComplete(void) {
- return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount;
-}
-
-void battleStateSelectionAiDecide(const uint8_t fighterIndex) {
- battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- const battlefighterteam_t enemyTeam =
- fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
- BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
-
- // Simple placeholder AI: attack the first living fighter on the
- // opposing team. No move variety or target scoring yet.
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- battlefighter_t *candidate = BATTLE.fighters[i];
- if(candidate == NULL) continue;
- if(candidate->team != enemyTeam) continue;
- if(!battleFighterIsAlive(candidate)) continue;
-
- battleStateSelectionQueueAttack(fighterIndex, i);
- return;
- }
-}
-
-void battleStateSelectionUpdate(void) {
- battlestateselection_t *state = &BATTLE.stateData.selection;
-
- // Are we still waiting for a selection?
- while(state->selectionIndex < BATTLE.fighterOrderCount) {
- const uint8_t fighterIndex = BATTLE.fighterOrder[state->selectionIndex];
-
- // Is this fighter still owed a decision?
- if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) {
- state->selectionIndex++;
- continue;
- }
-
- // Players will act on their own.
- battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
-
- battleStateSelectionAiDecide(fighterIndex);
- state->selectionIndex++;
- }
-}
-
-void battleStateSelectionDispose(void) {
-
-}
-
-void battleStateSelectionQueueAttack(
- const uint8_t fighterIndex,
- const uint8_t targetIndex
-) {
- assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB");
- assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "Target OOB");
- assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL");
- assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL");
-
- const battlemoveid_t move = battleMoveGetIdByName("slash");
- assertTrue(move != BATTLE_MOVE_ID_NULL, "Missing 'slash' battle move");
-
- battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
- if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++;
- action->move = move;
- action->targets[0] = targetIndex;
- action->user = BATTLE.fighters[fighterIndex];
-}
-
-battlefighter_t *battleStateSelectionGetCurrentFighter(void) {
- if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL;
-
- const uint8_t index = BATTLE.stateData.selection.selectionIndex;
- if(index >= BATTLE.fighterOrderCount) return NULL;
-
- return BATTLE.fighters[BATTLE.fighterOrder[index]];
-}
-
-void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
- battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
- if(fighter == NULL) return;
-
- const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex;
- const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex];
- battleStateSelectionQueueAttack(fighterIndex, targetIndex);
- BATTLE.stateData.selection.selectionIndex++;
-}
-
-void battleStateSelectionGoBack(void) {
- battlestateselection_t *state = &BATTLE.stateData.selection;
- uint8_t index = state->selectionIndex;
-
- while(index > 0) {
- index--;
-
- const uint8_t fighterIndex = BATTLE.fighterOrder[index];
- battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- if(fighter == NULL) continue;
- if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) continue;
- if(!battleStateSelectionFighterHasDecided(fighterIndex)) continue;
-
- battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
- action->move = BATTLE_MOVE_ID_NULL;
- BATTLE.actionsPendingCount--;
-
- state->selectionIndex = index;
- return;
- }
-}
-
-void battleStateSelectionPlayerFlee(void) {
- battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
- if(fighter == NULL) return;
-
- BATTLE.result = BATTLE_RESULT_FLED;
- battleSetState(BATTLE_STATE_FLED);
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h
deleted file mode 100644
index 3a43439e..00000000
--- a/src/dusk/rpg/battle/state/battlestateselection.h
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/fighter/battlefighter.h"
-
-typedef struct {
- // Cursor into BATTLE.fighters (by position index) for the fighter
- // currently deciding a move this round.
- uint8_t selectionIndex;
-} battlestateselection_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_SELECTION.
- */
-void battleStateSelectionInit(void);
-
-/**
- * Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see
- * battleStateSelectionAiDecide) for every AI-controlled fighter the
- * cursor passes, stopping once it reaches a player-controlled fighter
- * awaiting input (see battleStateSelectionGetCurrentFighter). Once every
- * position has decided, BATTLE_WAIT_SELECTION (see
- * cutsceneBattleWaitSelectionUpdate) queues the move to
- * BATTLE_STATE_EXECUTING.
- */
-void battleStateSelectionUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_SELECTION.
- */
-void battleStateSelectionDispose(void);
-
-/**
- * Checks whether the fighter at fighterIndex has already queued a move
- * this round.
- *
- * @param fighterIndex Index into BATTLE.fighters to check.
- * @return True if an action is already pending for this fighter.
- */
-bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex);
-
-/**
- * Checks whether the fighter at fighterIndex is a live, undecided
- * candidate that BATTLE_STATE_SELECTION should still be deciding a move
- * for.
- *
- * @param fighterIndex Index into BATTLE.fighters to check.
- * @return True if the fighter is alive and has no move queued yet.
- */
-bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex);
-
-/**
- * Decides and queues a move for an AI-controlled fighter: currently
- * always a basic attack against the first living fighter on the
- * opposing team (see battleStateSelectionQueueAttack). No-ops if there's
- * no living target (the battle should already be over by then via
- * BATTLE_POST_MOVE).
- *
- * @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
- */
-void battleStateSelectionAiDecide(const uint8_t fighterIndex);
-
-/**
- * Checks whether every fighter in BATTLE.fighterOrder has decided a move
- * this round.
- *
- * @return True once the selection cursor has passed every fighter.
- */
-bool_t battleStateSelectionIsComplete(void);
-
-/**
- * Queues a basic attack (the "slash" move) from fighterIndex against
- * targetIndex.
- *
- * @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
- * @param targetIndex Index into BATTLE.fighters of the target.
- */
-void battleStateSelectionQueueAttack(
- const uint8_t fighterIndex,
- const uint8_t targetIndex
-);
-
-/**
- * Returns the fighter currently awaiting a decision in
- * BATTLE_STATE_SELECTION.
- *
- * @return Pointer to the fighter awaiting a decision, or NULL if the
- * battle isn't in BATTLE_STATE_SELECTION or every position has
- * already decided.
- */
-battlefighter_t *battleStateSelectionGetCurrentFighter(void);
-
-/**
- * Submits the currently-selecting fighter's attack against a target, if
- * the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues
- * the move and advances the selection cursor.
- *
- * @param targetIndex Index into BATTLE.fighters of the target.
- */
-void battleStateSelectionPlayerAttack(const uint8_t targetIndex);
-
-/**
- * Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in
- * BATTLE_STATE_SELECTION and awaiting a decision.
- */
-void battleStateSelectionPlayerFlee(void);
-
-/**
- * Rewinds the selection cursor to the nearest earlier player-controlled
- * fighter that has already decided a move this round, clearing its
- * pending action so it can be re-decided. AI-decided fighters along the
- * way are left alone (skipped over, not rewound onto). No-ops if there's
- * no such fighter (e.g. already at the first decision of the round).
- */
-void battleStateSelectionGoBack(void);
\ No newline at end of file
diff --git a/src/dusk/rpg/battle/state/battlestatewon.c b/src/dusk/rpg/battle/state/battlestatewon.c
deleted file mode 100644
index 95d09f06..00000000
--- a/src/dusk/rpg/battle/state/battlestatewon.c
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "battlestatewon.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void battleStateWonInit(void) {
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_TEXT,
- .text = { .text = "You won the battle!" }
- },
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_ENDING }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-void battleStateWonUpdate(void) {
-
-}
-
-void battleStateWonDispose(void) {
-
-}
diff --git a/src/dusk/rpg/battle/state/battlestatewon.h b/src/dusk/rpg/battle/state/battlestatewon.h
deleted file mode 100644
index d9c72dec..00000000
--- a/src/dusk/rpg/battle/state/battlestatewon.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
-
-} battlestatewon_t;
-
-/**
- * Called when the battle enters BATTLE_STATE_WON. Queues a textbox
- * announcing the win, then transitions to BATTLE_STATE_ENDING once
- * dismissed.
- */
-void battleStateWonInit(void);
-
-/**
- * Updates BATTLE_STATE_WON for one frame.
- */
-void battleStateWonUpdate(void);
-
-/**
- * Called when the battle leaves BATTLE_STATE_WON.
- */
-void battleStateWonDispose(void);
diff --git a/src/dusk/rpg/cutscene/CMakeLists.txt b/src/dusk/rpg/cutscene/CMakeLists.txt
deleted file mode 100644
index 7655eb25..00000000
--- a/src/dusk/rpg/cutscene/CMakeLists.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscene.c
- cutsceneref.c
- cutscenesystem.c
-)
-
-# Subdirs
-add_subdirectory(item)
\ No newline at end of file
diff --git a/src/dusk/rpg/cutscene/cutscene.c b/src/dusk/rpg/cutscene/cutscene.c
deleted file mode 100644
index 0b03d15e..00000000
--- a/src/dusk/rpg/cutscene/cutscene.c
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscene.h"
-#include "item/json/cutscenejsonpauseflags.h"
-#include "util/memory.h"
-#include "assert/assert.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-
-errorret_t cutsceneParseItem(
- yyjson_val *itemObj,
- cutsceneitem_t *item,
- const uint8_t index
-) {
- assertNotNull(itemObj, "Item object cannot be NULL");
- assertNotNull(item, "Item cannot be NULL");
-
- if(!yyjson_is_obj(itemObj)) {
- errorThrow("Cutscene item %u is not an object", (uint32_t)index);
- }
-
- const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemObj, "type"));
- if(!cutsceneItemLookupType(typeName, &item->type)) {
- errorThrow(
- "Cutscene item %u: missing/unsupported item type %s",
- (uint32_t)index, typeName
- );
- }
-
- cutsceneitemloadcallback_t *load = CUTSCENE_ITEM_CALLBACKS[item->type].load;
- if(load == NULL) {
- errorThrow(
- "Cutscene item %u: unhandled item type %u",
- (uint32_t)index, (uint32_t)item->type
- );
- }
-
- errorret_t result = load(itemObj, item);
- if(errorIsNotOk(result)) errorChain(result);
-
- errorOk();
-}
-
-size_t cutsceneCountItems(yyjson_doc *doc) {
- assertNotNull(doc, "Doc cannot be NULL");
-
- yyjson_val *root = yyjson_doc_get_root(doc);
- yyjson_val *itemsArr = yyjson_obj_get(root, "items");
- if(!yyjson_is_arr(itemsArr)) return 0;
- return yyjson_arr_size(itemsArr);
-}
-
-errorret_t cutsceneParseDoc(
- yyjson_doc *doc,
- cutscene_t *scene,
- cutsceneitem_t *items,
- size_t itemsMax
-) {
- assertNotNull(doc, "Doc cannot be NULL");
- assertNotNull(scene, "Scene cannot be NULL");
- assertNotNull(items, "Items cannot be NULL");
- assertTrue(itemsMax > 0, "Items max cannot be 0");
-
- yyjson_val *root = yyjson_doc_get_root(doc);
- yyjson_val *itemsArr = yyjson_obj_get(root, "items");
- if(!yyjson_is_arr(itemsArr)) {
- errorThrow("Cutscene JSON is missing an 'items' array");
- }
-
- size_t itemCount = yyjson_arr_size(itemsArr);
- if(itemCount > itemsMax) {
- errorThrow("Cutscene has more items than its target buffer allows");
- }
-
- cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
- yyjson_val *pauseArr = yyjson_obj_get(root, "pause");
- if(yyjson_is_arr(pauseArr)) {
- if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) {
- errorThrow("Cutscene JSON has an invalid 'pause' flag");
- }
- }
-
- memoryZero(items, itemCount * sizeof(cutsceneitem_t));
-
- yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr);
- yyjson_val *itemVal;
- uint8_t index = 0;
- while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) {
- errorret_t itemResult = cutsceneParseItem(itemVal, &items[index], index);
- if(errorIsNotOk(itemResult)) errorChain(itemResult);
- index++;
- }
-
- scene->itemCount = (uint8_t)itemCount;
- scene->itemsMax = itemsMax;
- scene->pause = pause;
-
- errorOk();
-}
-
-void cutsceneAppendAt(
- cutscene_t *scene,
- const uint8_t atIndex,
- const cutsceneitem_t *items,
- const uint8_t count
-) {
- assertNotNull(scene, "Scene cannot be NULL");
- assertNotNull(items, "Items cannot be NULL");
- assertTrue(count > 0, "Count cannot be 0");
- assertTrue(atIndex <= scene->itemCount, "atIndex out of bounds");
- assertTrue(
- (size_t)scene->itemCount + count <= scene->itemsMax,
- "Cutscene item append exceeds itemsMax"
- );
-
- const size_t tailCount = (size_t)scene->itemCount - atIndex;
- if(tailCount > 0) {
- memoryMove(
- &scene->items[atIndex + count],
- &scene->items[atIndex],
- tailCount * sizeof(cutsceneitem_t)
- );
- }
- memoryCopy(&scene->items[atIndex], items, count * sizeof(cutsceneitem_t));
- scene->itemCount += count;
-}
-
-void cutsceneAppend(
- cutscene_t *scene,
- const cutsceneitem_t *items,
- const uint8_t count
-) {
- cutsceneAppendAt(scene, scene->itemCount, items, count);
-}
-
-void cutsceneAppendNext(
- cutscene_t *scene,
- const uint8_t currentIndex,
- const cutsceneitem_t *items,
- const uint8_t count
-) {
- cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count);
-}
-
-errorret_t cutsceneLoadParse(
- const char_t *path,
- cutscene_t *scene,
- cutsceneitem_t *items,
- const size_t itemsMax
-) {
- assertNotNull(path, "Path cannot be NULL");
- assertNotNull(scene, "Scene cannot be NULL");
- assertNotNull(items, "Items cannot be NULL");
-
- // Lock the asset, require it loaded.
- assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
- errorret_t result = assetRequireLoaded(entry);
- if(errorIsNotOk(result)) {
- assetUnlockEntry(entry);
- errorChain(result);
- }
-
- // Parse the loaded JSON
- errorret_t parseResult = cutsceneParseDoc(
- entry->data.json, scene, items, itemsMax
- );
-
- // Free up the asset entry.
- assetUnlockEntry(entry);
- errorChain(parseResult);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h
deleted file mode 100644
index 07939299..00000000
--- a/src/dusk/rpg/cutscene/cutscene.h
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenepause.h"
-
-typedef struct cutscene_s {
- cutsceneitem_t *items;
- uint8_t itemCount;
- // Capacity of items - see cutsceneAppend/cutsceneAppendAt/
- // cutsceneAppendNext, which assert against this rather than taking a
- // capacity parameter of their own. Set once, either by cutsceneParseDoc
- // (to whatever itemsMax it was given) or by whoever declares a
- // C-authored cutscene_t directly (to items' true array capacity, which
- // may be larger than itemCount to leave room for a later append/insert).
- size_t itemsMax;
- cutscenepause_t pause;
-} cutscene_t;
-
-/**
- * Parses one cutscene item object into item. Every field/enum name is
- * validated as real (untrusted, authored) file content - failures are
- * real errors, not asserts. Dispatches to the matching type's Load
- * callback (see CUTSCENE_ITEM_CALLBACKS in item/cutsceneitem.h) - the
- * per-type parsing logic itself lives there, colocated with that type's
- * runtime behavior.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @param index Index of this item within the cutscene, for error messages.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneParseItem(
- yyjson_val *itemObj,
- cutsceneitem_t *item,
- const uint8_t index
-);
-
-/**
- * Counts how many entries are in doc's top-level "items" array, without
- * parsing any of them - e.g. so a caller that wants to malloc its own
- * items array instead of using a fixed one could size it first (we don't
- * do that ourselves, but the option stays open).
- *
- * @param doc Already-parsed JSON document.
- * @return Number of entries in "items", or 0 if it's missing/not an array.
- */
-size_t cutsceneCountItems(yyjson_doc *doc);
-
-/**
- * Parses doc's "items" (and optional "pause") into items, and updates
- * scene's itemCount/itemsMax/pause - scene->items is left untouched, since
- * the caller already points it at items. Purely a parser: doesn't touch
- * the asset system, doesn't lock/load anything, and doesn't take
- * ownership of doc - the caller is responsible for both loading doc
- * beforehand and for its lifetime afterward (though as of cutscene items
- * owning their own string fields by value, nothing parsed here actually
- * needs doc to survive past this call returning).
- *
- * @param doc Already-parsed JSON document to read from.
- * @param scene Destination scene - itemCount/itemsMax/pause are
- * overwritten.
- * @param items Destination item array, capacity itemsMax.
- * @param itemsMax Capacity of items.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneParseDoc(
- yyjson_doc *doc,
- cutscene_t *scene,
- cutsceneitem_t *items,
- size_t itemsMax
-);
-
-/**
- * Inserts items into scene->items starting at atIndex, growing
- * scene->itemCount. Every existing item from atIndex onward shifts right
- * by count to make room. Asserts if this would exceed scene->itemsMax.
- *
- * @param scene Cutscene to insert into.
- * @param atIndex Index the first inserted item should end up at - must be
- * <= scene->itemCount.
- * @param items Items to insert, copied in order.
- * @param count Number of entries in items.
- */
-void cutsceneAppendAt(
- cutscene_t *scene,
- const uint8_t atIndex,
- const cutsceneitem_t *items,
- const uint8_t count
-);
-
-/**
- * Same as cutsceneAppendAt, but always inserts at the end of
- * scene->items.
- *
- * @param scene Cutscene to append to.
- * @param items Items to append, copied in order.
- * @param count Number of entries in items.
- */
-void cutsceneAppend(
- cutscene_t *scene,
- const cutsceneitem_t *items,
- const uint8_t count
-);
-
-/**
- * Same as cutsceneAppendAt, but inserts right after currentIndex (i.e. at
- * currentIndex + 1) instead of at a literal index - e.g. pass
- * CUTSCENE_SYSTEM.currentItem to make these items run immediately after
- * the currently-executing one. currentIndex may be 0xFF (nothing has run
- * yet), which wraps to insert at index 0, matching that sentinel's usual
- * meaning elsewhere in the cutscene system.
- *
- * @param scene Cutscene to insert into.
- * @param currentIndex Index to insert after.
- * @param items Items to insert, copied in order.
- * @param count Number of entries in items.
- */
-void cutsceneAppendNext(
- cutscene_t *scene,
- const uint8_t currentIndex,
- const cutsceneitem_t *items,
- const uint8_t count
-);
-
-/**
- * Loads a cutscene JSON asset by path and parses it into scene/items (see
- * cutsceneParseDoc) - the common "lock the asset, require it loaded,
- * parse its 'items' array, unlock it again" sequence shared by every
- * place that resolves a cutscene by file path (cutsceneSystemLoad,
- * cutsceneCutsceneResolve, cutsceneInsertResolveAndSplice,
- * battleStateExecutingInit, cutsceneSystemGoTo's reload). Purely propagates the
- * error on failure (like cutsceneParseDoc) - this is core cutscene logic,
- * not UI, so it doesn't reach for uiFatalErrorOpen itself; each caller
- * decides how to surface the failure (typically errorCatch(errorPrint())
- * + uiFatalErrorOpen(NULL), same as any other asset load failure).
- *
- * @param path Asset path of the cutscene JSON file to load.
- * @param scene Destination scene - itemCount/itemsMax/pause overwritten.
- * @param items Destination item array, capacity itemsMax.
- * @param itemsMax Capacity of items.
- * @return Error code indicating success or failure of the load/parse.
- */
-errorret_t cutsceneLoadParse(
- const char_t *path,
- cutscene_t *scene,
- cutsceneitem_t *items,
- const size_t itemsMax
-);
diff --git a/src/dusk/rpg/cutscene/cutscenepause.h b/src/dusk/rpg/cutscene/cutscenepause.h
deleted file mode 100644
index e9aad3fd..00000000
--- a/src/dusk/rpg/cutscene/cutscenepause.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef uint8_t cutscenepause_t;
-
-#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0)
-#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
-#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
-#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
-#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
-
-#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
- CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
-))
-
-#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
- CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \
- CUTSCENE_PAUSE_BATTLE \
-))
diff --git a/src/dusk/rpg/cutscene/cutsceneref.c b/src/dusk/rpg/cutscene/cutsceneref.c
deleted file mode 100644
index 095965e9..00000000
--- a/src/dusk/rpg/cutscene/cutsceneref.c
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutsceneref.h"
-#include "assert/assert.h"
-
-void cutsceneRefReset(
- cutsceneref_t *refs,
- entity_t *interact,
- entity_t *interacted
-) {
- assertNotNull(refs, "Refs cannot be null");
-
- refs->entityInteract = interact;
- refs->entityInteracted = interacted;
- refs->entityLastCreated = NULL;
- refs->entityLastRef = NULL;
- refs->areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
- refs->textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
-}
diff --git a/src/dusk/rpg/cutscene/cutsceneref.h b/src/dusk/rpg/cutscene/cutsceneref.h
deleted file mode 100644
index 7197460c..00000000
--- a/src/dusk/rpg/cutscene/cutsceneref.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct entity_s entity_t;
-
-#define CUTSCENE_REF_AREA_LAST_CREATED ((uint8_t)0xFF)
-#define CUTSCENE_REF_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
-
-typedef struct {
- entity_t *entityInteract;
- entity_t *entityInteracted;
- entity_t *entityLastCreated;
- entity_t *entityLastRef;
- uint8_t areaLastCreated;
- uint8_t textMiniLastCreated;
-} cutsceneref_t;
-
-/**
- * Resets refs to an empty state.
- *
- * @param refs Refs to reset.
- * @param interact The entity that initiated the interaction, or NULL.
- * @param interacted The entity being interacted with, or NULL.
- */
-void cutsceneRefReset(
- cutsceneref_t *refs,
- entity_t *interact,
- entity_t *interacted
-);
diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c
deleted file mode 100644
index a68c8cdf..00000000
--- a/src/dusk/rpg/cutscene/cutscenesystem.c
+++ /dev/null
@@ -1,358 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "assert/assert.h"
-#include "ui/overlay/uifatalerror.h"
-#include "save/slot/saveslot.h"
-
-cutscenesystem_t CUTSCENE_SYSTEM;
-
-void cutsceneSystemInit() {
- memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
- CUTSCENE_SYSTEM.loadedScene.items = CUTSCENE_SYSTEM.loadedItems;
- CUTSCENE_SYSTEM.loadedScene.itemsMax = CUTSCENE_LOADED_ITEMS_MAX;
-}
-
-void cutsceneSystemUpdate() {
- if(CUTSCENE_SYSTEM.scene == NULL) return;
-
- const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
- if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext();
-}
-
-void cutsceneSystemDispose() {
- CUTSCENE_SYSTEM.scene = NULL;
- CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE;
- CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
- cutsceneRefReset(&CUTSCENE_SYSTEM.refs, NULL, NULL);
- CUTSCENE_SYSTEM.textCache[0] = '\0';
- CUTSCENE_SYSTEM.onComplete = NULL;
- CUTSCENE_SYSTEM.loadedFile[0] = '\0';
-}
-
-void cutsceneSystemPrepare(
- cutscene_t *cutscene,
- entity_t *interact,
- entity_t *interacted
-) {
- assertNotNull(cutscene, "Cutscene cannot be NULL");
-
- CUTSCENE_SYSTEM.scene = cutscene;
- CUTSCENE_SYSTEM.pause = cutscene->pause;
- cutsceneRefReset(&CUTSCENE_SYSTEM.refs, interact, interacted);
- CUTSCENE_SYSTEM.textCache[0] = '\0';
- CUTSCENE_SYSTEM.onComplete = NULL;
-
- // cutsceneNext will cause overflow to index 0 (intentional).
- CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE;
-
- // Clear loaded file if cutscene not running from file.
- if(cutscene != &CUTSCENE_SYSTEM.loadedScene) {
- CUTSCENE_SYSTEM.loadedFile[0] = '\0';
- }
-}
-
-void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
- cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
-}
-
-void cutsceneSystemStartCutsceneWith(
- cutscene_t *cutscene,
- entity_t *interact,
- entity_t *interacted
-) {
- assertNotNull(cutscene, "Cutscene cannot be NULL");
- assertTrue(
- cutscene->itemCount <= cutscene->itemsMax,
- "Cutscene itemCount exceeds itemsMax"
- );
-
- // Prep cutscene, save it to the save slot, and advance, aka overflow to 0.
- cutsceneSystemPrepare(cutscene, interact, interacted);
- saveSlotSetCurrentCutscene(CUTSCENE_SYSTEM.loadedFile);
- cutsceneSystemNext();
-}
-
-void cutsceneSystemStartCutsceneAndGoToMarker(
- cutscene_t *cutscene,
- const char_t *marker
-) {
- assertNotNull(marker, "Marker cannot be NULL");
-
- // Restarts the cutscene
- cutsceneSystemPrepare(cutscene, NULL, NULL);
- cutsceneSystemGoTo(marker);
-}
-
-void cutsceneSystemInsertItemsNext(
- const cutsceneitem_t *items,
- const size_t itemCount
-) {
- assertNotNull(items, "Items cannot be NULL");
- assertNotNull(
- CUTSCENE_SYSTEM.scene,
- "cutsceneSystemInsertItemsNext called with no cutscene running"
- );
- cutsceneAppendNext(
- CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount
- );
-}
-
-void cutsceneSystemInsertItems(
- const cutsceneitem_t *items,
- const size_t itemCount
-) {
- cutsceneSystemInsertItemsNext(items, itemCount);
- cutsceneSystemNext();
-}
-
-void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
- assertNotNull(cutscene, "Cutscene cannot be NULL");
- cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
-}
-
-void cutsceneSystemClearAfter(const uint8_t index) {
- assertNotNull(
- CUTSCENE_SYSTEM.scene,
- "cutsceneSystemClearAfter called with no cutscene running"
- );
- assertTrue(
- index < CUTSCENE_SYSTEM.scene->itemCount,
- "cutsceneSystemClearAfter index out of range"
- );
-
- CUTSCENE_SYSTEM.scene->itemCount = index + 1;
-}
-
-void cutsceneSystemLoad(const char_t *file) {
- assertNotNull(file, "File cannot be NULL");
-
- // Take a copy of file, as it may be a part of the cutscene itself.
- char_t fileCopy[ASSET_FILE_NAME_MAX];
- stringCopy(fileCopy, file, ASSET_FILE_NAME_MAX - 1);
-
- // Load the cutscene asset, parse it, and start it.
- errorret_t result = cutsceneLoadParse(
- fileCopy, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
- CUTSCENE_LOADED_ITEMS_MAX
- );
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(NULL);
- return;
- }
-
- // Store the loaded file so we can restart if needed.
- stringCopy(CUTSCENE_SYSTEM.loadedFile, fileCopy, ASSET_FILE_NAME_MAX - 1);
- cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
-}
-
-void cutsceneRestart(void) {
- assertNotNull(
- CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
- );
- assertTrue(
- CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene,
- "cutsceneRestart called with a cutscene not loaded from file"
- );
- assertTrue(
- CUTSCENE_SYSTEM.loadedFile[0] != '\0',
- "cutsceneRestart called with no loaded file"
- );
-
- // Copy the refs and callbacks
- cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
- cutsceneref_t refs = CUTSCENE_SYSTEM.refs;
-
- // Reload the file.
- char_t file[ASSET_FILE_NAME_MAX];
- stringCopy(file, CUTSCENE_SYSTEM.loadedFile, ASSET_FILE_NAME_MAX - 1);
- cutsceneSystemLoad(file);
-
- // Restore the refs and callbacks.
- CUTSCENE_SYSTEM.onComplete = onComplete;
- CUTSCENE_SYSTEM.refs = refs;
-}
-
-void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
- assertNotNull(
- CUTSCENE_SYSTEM.scene,
- "cutsceneSystemSetOnComplete called with no cutscene running"
- );
- CUTSCENE_SYSTEM.onComplete = onComplete;
-}
-
-entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
- entity_t *entity;
-
- if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
- assertNotNull(
- CUTSCENE_SYSTEM.refs.entityInteract,
- "CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
- );
- entity = CUTSCENE_SYSTEM.refs.entityInteract;
- } else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
- assertNotNull(
- CUTSCENE_SYSTEM.refs.entityInteracted,
- "CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
- );
- entity = CUTSCENE_SYSTEM.refs.entityInteracted;
- } else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
- assertNotNull(
- CUTSCENE_SYSTEM.refs.entityLastCreated,
- "CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
- );
- entity = CUTSCENE_SYSTEM.refs.entityLastCreated;
- } else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
- assertNotNull(
- CUTSCENE_SYSTEM.refs.entityLastRef,
- "CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
- );
- entity = CUTSCENE_SYSTEM.refs.entityLastRef;
- } else {
- assertTrue(
- entityIndex < ENTITY_COUNT,
- "Entity index is out of range"
- );
- entity = &ENTITIES[entityIndex];
- }
-
- CUTSCENE_SYSTEM.refs.entityLastRef = entity;
- return entity;
-}
-
-uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
- if(areaId == CUTSCENE_REF_AREA_LAST_CREATED) {
- assertTrue(
- CUTSCENE_SYSTEM.refs.areaLastCreated != CUTSCENE_REF_AREA_LAST_CREATED,
- "CUTSCENE_REF_AREA_LAST_CREATED used but no map area has been created"
- );
- return CUTSCENE_SYSTEM.refs.areaLastCreated;
- }
- return areaId;
-}
-
-uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
- if(index == CUTSCENE_REF_TEXT_MINI_LAST_CREATED) {
- assertTrue(
- CUTSCENE_SYSTEM.refs.textMiniLastCreated !=
- CUTSCENE_REF_TEXT_MINI_LAST_CREATED,
- "CUTSCENE_REF_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
- "shown"
- );
- return CUTSCENE_SYSTEM.refs.textMiniLastCreated;
- }
- return index;
-}
-
-const char_t * cutsceneSystemGetTextCache(void) {
- return CUTSCENE_SYSTEM.textCache;
-}
-
-void cutsceneSystemSetTextCache(const char_t *text) {
- assertNotNull(text, "Text cannot be NULL");
-
- stringCopy(
- CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1
- );
-}
-
-void cutsceneSystemNext() {
- if(CUTSCENE_SYSTEM.scene == NULL) return;
-
- // Pop the just-finished item off the front.
- if(CUTSCENE_SYSTEM.currentItem == CUTSCENE_CURRENT_ITEM_NONE) {
- CUTSCENE_SYSTEM.currentItem = 0;
- } else {
- cutscene_t *scene = CUTSCENE_SYSTEM.scene;
- const size_t tailCount = (size_t)scene->itemCount - 1;
- if(tailCount > 0) {
- memoryMove(
- &scene->items[0], &scene->items[1], tailCount * sizeof(cutsceneitem_t)
- );
- }
- scene->itemCount--;
- }
-
- // End of the cutscene?
- if(CUTSCENE_SYSTEM.scene->itemCount == 0) {
- cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
-
- CUTSCENE_SYSTEM.scene = NULL;
- CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
-
- if(onComplete != NULL) onComplete(NULL);
- return;
- }
-
- // Start next (now current) item
- const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
- memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data));
- cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
-}
-
-void cutsceneSystemGoTo(const char_t *name) {
- assertNotNull(name, "Name cannot be NULL");
- assertNotNull(
- CUTSCENE_SYSTEM.scene, "cutsceneSystemGoTo called with no cutscene running"
- );
- assertTrue(
- CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene,
- "cutsceneSystemGoTo requires the running cutscene file reference (1)"
- );
- assertTrue(
- CUTSCENE_SYSTEM.loadedFile[0] != '\0',
- "cutsceneSystemGoTo requires the running cutscene file reference (2)"
- );
-
- cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems };
- errorret_t result = cutsceneLoadParse(
- CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems,
- CUTSCENE_LOADED_ITEMS_MAX
- );
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(NULL);
- return;
- }
-
- // Search for the marker with the given name...
- for(uint8_t i = 0; i < freshScene.itemCount; i++) {
- const cutsceneitem_t *item = &freshScene.items[i];
- if(
- item->type != CUTSCENE_ITEM_TYPE_MARKER ||
- !stringEquals(item->marker.name, name)
- ) continue;
-
- // Found, so splice the cutscene at that point and start it, the remaining
- // items being the rest of the cutscene after the marker.
- const uint8_t remaining = freshScene.itemCount - i;
- memoryCopy(
- CUTSCENE_SYSTEM.loadedItems, &freshScene.items[i],
- remaining * sizeof(cutsceneitem_t)
- );
- CUTSCENE_SYSTEM.loadedScene.itemCount = remaining;
- CUTSCENE_SYSTEM.currentItem = 0;
-
- memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
- cutsceneItemStart(
- &CUTSCENE_SYSTEM.loadedItems[0], &CUTSCENE_SYSTEM.data
- );
- return;
- }
-
- assertTrue(false, "cutsceneSystemGoTo: no marker found with that name");
-}
-
-const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
- if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
- return &CUTSCENE_SYSTEM.scene->items[0];
-}
diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h
deleted file mode 100644
index 3921d549..00000000
--- a/src/dusk/rpg/cutscene/cutscenesystem.h
+++ /dev/null
@@ -1,241 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "cutscene.h"
-#include "cutsceneref.h"
-
-#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
-#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
-#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
-#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
-#define CUTSCENE_CURRENT_ITEM_NONE ((uint8_t)0xFF)
-
-#define CUTSCENE_TEXT_CACHE_MAX 64
-#define CUTSCENE_LOADED_ITEMS_MAX 128
-#define CUTSCENE_INSERT_ITEMS_MAX 16
-
-typedef struct {
- cutscene_t *scene;
- uint8_t currentItem;
- cutscenepause_t pause;
-
- cutsceneref_t refs;
-
- // Free-form text cache for the running cutscene - see
- // cutsceneSystemGetTextCache/cutsceneSystemSetTextCache.
- char_t textCache[CUTSCENE_TEXT_CACHE_MAX];
-
- // Runtime data for the current item.
- cutsceneitemdata_t data;
-
- // See cutsceneSystemSetOnComplete.
- cutscenecallback_t onComplete;
-
- // Cache and storage used for the dynamically loaded cutscenes.
- cutsceneitem_t loadedItems[CUTSCENE_LOADED_ITEMS_MAX];
- cutscene_t loadedScene;
- char_t loadedFile[ASSET_FILE_NAME_MAX];
-
- // Cutscene items scratch, mostly used for CUTSCENE_INSERT items.
- cutsceneitem_t gotoScratchItems[CUTSCENE_LOADED_ITEMS_MAX];
-} cutscenesystem_t;
-
-extern cutscenesystem_t CUTSCENE_SYSTEM;
-
-/**
- * Initialize the cutscene system.
- */
-void cutsceneSystemInit();
-
-/**
- * Disposes of the cutscene system, stopping any active cutscene.
- */
-void cutsceneSystemDispose();
-
-/**
- * Resets CUTSCENE_SYSTEM to run cutscene from its first item, binding
- * interact/interacted entities. Does not itself advance to the first item.
- *
- * @param cutscene Pointer to the cutscene to prepare.
- * @param interact The entity that initiated the interaction (player), or
- * NULL.
- * @param interacted The entity being interacted with (NPC), or NULL.
- */
-void cutsceneSystemPrepare(
- cutscene_t *cutscene,
- entity_t *interact,
- entity_t *interacted
-);
-
-/**
- * Start a cutscene with no bound entities.
- *
- * @param cutscene Pointer to the cutscene to start.
- */
-void cutsceneSystemStartCutscene(cutscene_t *cutscene);
-
-/**
- * Start a cutscene with the two entities that triggered it.
- *
- * @param cutscene Pointer to the cutscene to start.
- * @param interact The entity that initiated the interaction (player).
- * @param interacted The entity that was interacted with (NPC).
- */
-void cutsceneSystemStartCutsceneWith(
- cutscene_t *cutscene,
- entity_t *interact,
- entity_t *interacted
-);
-
-/**
- * Starts a cutscene with no bound entities, jumping straight to the
- * marker with the given name instead of running from the first item.
- * Asserts if no marker with that name exists.
- *
- * @param cutscene Pointer to the cutscene to start.
- * @param marker Marker name to jump to.
- */
-void cutsceneSystemStartCutsceneAndGoToMarker(
- cutscene_t *cutscene,
- const char_t *marker
-);
-
-/**
- * Splices items into the running cutscene so that items[0] becomes the
- * very next item, without otherwise disturbing the currently-running
- * item's progress.
- *
- * @param items Items to splice in.
- * @param itemCount Number of items in items.
- */
-void cutsceneSystemInsertItemsNext(
- const cutsceneitem_t *items,
- const size_t itemCount
-);
-
-/**
- * Splices items into the running cutscene right after the current item
- * (see cutsceneSystemInsertItemsNext), then immediately advances into the
- * first spliced item.
- *
- * @param items Items to splice in.
- * @param itemCount Number of items in items.
- */
-void cutsceneSystemInsertItems(
- const cutsceneitem_t *items,
- const size_t itemCount
-);
-
-/**
- * Splices cutscene's items into the running cutscene.
- *
- * @param cutscene The cutscene whose items to splice in.
- */
-void cutsceneSystemInsertCutscene(const cutscene_t *cutscene);
-
-/**
- * Discards every item queued after index (e.g. index 0 keeps only the
- * currently-running item and drops everything spliced in behind it),
- * without disturbing index's own progress - e.g. so a callback that
- * decides the rest of a batch-spliced sequence is now moot (a battle
- * ending mid-round, with other fighters' still-queued move cutscenes and
- * the round's trailing state transition left behind) can drop it before
- * splicing in its own follow-up items, instead of leaving it to run
- * afterward against torn-down state.
- *
- * @param index Index to keep - everything after it is discarded. Must be
- * < the running cutscene's current item count.
- */
-void cutsceneSystemClearAfter(const uint8_t index);
-
-/**
- * Loads and immediately starts a cutscene asset by file name.
- *
- * @param file Cutscene file name.
- */
-void cutsceneSystemLoad(const char_t *file);
-
-/**
- * Restarts the currently running cutscene. Will perform a load if the cutscene
- * was initially from a file.
- */
-void cutsceneRestart(void);
-
-/**
- * Sets a native callback to fire once when the currently running cutscene
- * finishes.
- *
- * @param onComplete Callback to fire on completion.
- */
-void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
-
-/**
- * Resolves a raw entity index (or sentinel) to an entity pointer.
- *
- * @param entityIndex Raw entity index or sentinel value.
- * @returns Pointer to the resolved entity.
- */
-entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
-
-/**
- * Resolves a raw map area ID (or CUTSCENE_REF_AREA_LAST_CREATED sentinel) to
- * a concrete map area ID.
- *
- * @param areaId Raw map area ID or sentinel value.
- * @returns The resolved map area ID.
- */
-uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
-
-/**
- * Resolves a raw mini textbox slot index (or CUTSCENE_REF_TEXT_MINI_LAST_CREATED
- * sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
- *
- * @param index Raw slot index or sentinel value.
- * @returns The resolved slot index.
- */
-uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
-
-/**
- * Returns the running cutscene's cached text - see
- * cutsceneSystemSetTextCache. Empty ("") if nothing has been cached yet.
- *
- * @returns The cached text.
- */
-const char_t * cutsceneSystemGetTextCache(void);
-
-/**
- * Overwrites the running cutscene's cached text.
- *
- * @param text The text to cache; copied internally.
- */
-void cutsceneSystemSetTextCache(const char_t *text);
-
-/**
- * Advance to the next item in the cutscene.
- */
-void cutsceneSystemNext();
-
-/**
- * Jumps to a cutscene marker directly. If the cutscene is a loaded cutscene,
- * this will result in it being reloaded (due to unshift nature of cutscenes).
- *
- * @param name Marker name to search for.
- */
-void cutsceneSystemGoTo(const char_t *name);
-
-/**
- * Update the cutscene system for one frame.
- */
-void cutsceneSystemUpdate();
-
-/**
- * Get the current cutscene item.
- *
- * @return Pointer to the current cutscene item.
- */
-const cutsceneitem_t * cutsceneSystemGetCurrentItem();
diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt
deleted file mode 100755
index bffc8bf0..00000000
--- a/src/dusk/rpg/cutscene/item/CMakeLists.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutsceneitem.c
- cutsceneitembase.c
- cutscenecallback.c
- cutsceneprint.c
-)
-
-add_subdirectory(control)
-add_subdirectory(entity)
-add_subdirectory(item)
-add_subdirectory(maparea)
-add_subdirectory(ui)
-add_subdirectory(battle)
-add_subdirectory(save)
-add_subdirectory(audio)
-add_subdirectory(json)
-add_subdirectory(story)
-add_subdirectory(party)
diff --git a/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt b/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt
deleted file mode 100644
index 2fdb6a5b..00000000
--- a/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutsceneaudioplay.c
- cutsceneaudiostop.c
- cutsceneaudiopause.c
- cutsceneaudioresume.c
- cutsceneaudiofade.c
- cutsceneaudiofadewait.c
- cutsceneaudiosetpan.c
- cutsceneaudiosetloop.c
- cutsceneaudioset.c
-)
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c
deleted file mode 100644
index fbacb232..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-#include "rpg/cutscene/item/json/cutscenejsoneasing.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneAudioFadeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerFadeTo(
- item->audioFade.channel,
- item->audioFade.from,
- item->audioFade.to,
- item->audioFade.duration,
- item->audioFade.easing
- );
-}
-
-bool_t cutsceneAudioFadeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioFadeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- audiomixerchannel_t channelValue;
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue
- )) {
- errorThrow("Cutscene item (AUDIO_FADE): invalid 'channel'");
- }
- item->audioFade.channel = channelValue;
- item->audioFade.from = cutsceneJsonGetFloat(itemObj, "from", 0.0f);
- item->audioFade.to = cutsceneJsonGetFloat(itemObj, "to", 0.0f);
- item->audioFade.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f);
-
- const char_t *easingName =
- yyjson_get_str(yyjson_obj_get(itemObj, "easing"));
- easingtype_t easingValue = EASING_LINEAR;
- if(easingName != NULL &&
- !cutsceneJsonLookupEasing(easingName, &easingValue)) {
- errorThrow("Cutscene item (AUDIO_FADE): invalid 'easing'");
- }
- item->audioFade.easing = easingValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h
deleted file mode 100644
index d6845c85..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "animation/easing.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- audiomixerchannel_t channel;
- float_t from;
- float_t to;
- float_t duration;
- easingtype_t easing;
-} cutsceneaudiofade_t;
-
-/**
- * Starts an audio fade step (starts the fade via audioMixerFadeTo() -
- * doesn't wait for it; pair with CUTSCENE_AUDIO_FADE_WAIT to block on it).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioFadeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio fade step (always completes immediately - the fade
- * itself keeps running independently of cutscene progression).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioFadeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_FADE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioFadeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c
deleted file mode 100644
index fd8a0912..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-
-bool_t cutsceneAudioFadeWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return !audioMixerIsFading(item->audioFadeWaitChannel);
-}
-
-errorret_t cutsceneAudioFadeWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioFadeWaitChannel);
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h
deleted file mode 100644
index 68a3ada7..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Updates an audio fade-wait step, completing once the watched channel's
- * fade (started by a CUTSCENE_AUDIO_FADE elsewhere) finishes. Has no Start
- * callback -- there's nothing to do until the fade is actually done.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the channel is no longer fading.
- */
-bool_t cutsceneAudioFadeWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_FADE_WAIT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioFadeWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c
deleted file mode 100644
index 7b098c3c..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-
-void cutsceneAudioPauseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerPause(item->audioPauseChannel);
-}
-
-bool_t cutsceneAudioPauseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioPauseLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioPauseChannel);
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h
deleted file mode 100644
index 787f1498..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts an audio pause step (pauses the channel immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioPauseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio pause step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioPauseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_PAUSE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioPauseLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c
deleted file mode 100644
index e742a20b..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneAudioPlayStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerQueuePlay(
- item->audioPlay.file,
- item->audioPlay.channel,
- item->audioPlay.volume,
- item->audioPlay.pan,
- item->audioPlay.looping,
- item->audioPlay.loopCount,
- item->audioPlay.loopStart,
- item->audioPlay.loopTo
- );
-}
-
-bool_t cutsceneAudioPlayUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioPlayLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonCopyString(
- itemObj, "file", NULL, item->audioPlay.file, AUDIO_PATH_MAX
- )) {
- errorThrow("Cutscene item (AUDIO_PLAY): missing/too-long 'file'");
- }
- audiomixerchannel_t channelValue;
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue
- )) {
- errorThrow("Cutscene item (AUDIO_PLAY): invalid 'channel'");
- }
- item->audioPlay.channel = channelValue;
-
- item->audioPlay.volume = cutsceneJsonGetFloat(itemObj, "volume", 1.0f);
- item->audioPlay.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f);
- item->audioPlay.looping = cutsceneJsonGetBool(itemObj, "looping", false);
- item->audioPlay.loopCount =
- (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0);
- item->audioPlay.loopStart =
- cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f);
- item->audioPlay.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h
deleted file mode 100644
index 5348af96..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- char_t file[AUDIO_PATH_MAX];
- audiomixerchannel_t channel;
- float_t volume;
- float_t pan;
- bool_t looping;
- uint8_t loopCount;
- float_t loopStart;
- float_t loopTo;
-} cutsceneaudioplay_t;
-
-/**
- * Starts an audio play step (queues the sound via audioMixerQueuePlay() -
- * see its own comment on when it actually starts).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioPlayStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio play step (always completes immediately - the sound
- * keeps loading/playing independently of cutscene progression).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioPlayUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_PLAY cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioPlayLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c
deleted file mode 100644
index c6ab25dc..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-
-void cutsceneAudioResumeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerResume(item->audioResumeChannel);
-}
-
-bool_t cutsceneAudioResumeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioResumeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioResumeChannel);
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h
deleted file mode 100644
index 91740eb2..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts an audio resume step (resumes the channel immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioResumeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio resume step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioResumeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_RESUME cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioResumeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c
deleted file mode 100644
index 2e6ef12a..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneAudioSetStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerSetPan(item->audioSet.channel, item->audioSet.pan);
- audioMixerSetLoop(
- item->audioSet.channel,
- item->audioSet.looping,
- item->audioSet.loopCount,
- item->audioSet.loopStart,
- item->audioSet.loopTo
- );
-}
-
-bool_t cutsceneAudioSetUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioSetLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- audiomixerchannel_t channelValue;
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue
- )) {
- errorThrow("Cutscene item (AUDIO_SET): invalid 'channel'");
- }
- item->audioSet.channel = channelValue;
- item->audioSet.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f);
- item->audioSet.looping = cutsceneJsonGetBool(itemObj, "looping", true);
- item->audioSet.loopCount =
- (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0);
- item->audioSet.loopStart =
- cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f);
- item->audioSet.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h
deleted file mode 100644
index 648dfad2..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- audiomixerchannel_t channel;
- float_t pan;
- bool_t looping;
- uint8_t loopCount;
- float_t loopStart;
- float_t loopTo;
-} cutsceneaudioset_t;
-
-/**
- * Starts an audio set step - applies pan and looping behaviour together in
- * one item (via audioMixerSetPan()/audioMixerSetLoop()), for when you want
- * to change both at once rather than using CUTSCENE_AUDIO_SET_PAN/
- * CUTSCENE_AUDIO_SET_LOOP separately.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioSetStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio set step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioSetUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_SET cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioSetLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c
deleted file mode 100644
index 101c563b..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneAudioSetLoopStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerSetLoop(
- item->audioSetLoop.channel,
- item->audioSetLoop.looping,
- item->audioSetLoop.loopCount,
- item->audioSetLoop.loopStart,
- item->audioSetLoop.loopTo
- );
-}
-
-bool_t cutsceneAudioSetLoopUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioSetLoopLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- audiomixerchannel_t channelValue;
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue
- )) {
- errorThrow("Cutscene item (AUDIO_SET_LOOP): invalid 'channel'");
- }
- item->audioSetLoop.channel = channelValue;
- item->audioSetLoop.looping = cutsceneJsonGetBool(itemObj, "looping", true);
- item->audioSetLoop.loopCount =
- (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0);
- item->audioSetLoop.loopStart =
- cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f);
- item->audioSetLoop.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h
deleted file mode 100644
index 5f964719..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- audiomixerchannel_t channel;
- bool_t looping;
- uint8_t loopCount;
- float_t loopStart;
- float_t loopTo;
-} cutsceneaudiosetloop_t;
-
-/**
- * Starts an audio set-loop step (applies the new looping behaviour
- * immediately, via audioMixerSetLoop()).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioSetLoopStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio set-loop step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioSetLoopUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_SET_LOOP cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioSetLoopLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c
deleted file mode 100644
index 04cef26d..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneAudioSetPanStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerSetPan(item->audioSetPan.channel, item->audioSetPan.pan);
-}
-
-bool_t cutsceneAudioSetPanUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioSetPanLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- audiomixerchannel_t channelValue;
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue
- )) {
- errorThrow("Cutscene item (AUDIO_SET_PAN): invalid 'channel'");
- }
- item->audioSetPan.channel = channelValue;
- item->audioSetPan.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h
deleted file mode 100644
index bbc62cd7..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- audiomixerchannel_t channel;
- float_t pan;
-} cutsceneaudiosetpan_t;
-
-/**
- * Starts an audio set-pan step (applies the new pan immediately, via
- * audioMixerSetPan()).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioSetPanStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio set-pan step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioSetPanUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_SET_PAN cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioSetPanLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c
deleted file mode 100644
index 0666a89d..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "audio/mixer/audiomixer.h"
-#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h"
-
-void cutsceneAudioStopStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- audioMixerStop(item->audioStopChannel);
-}
-
-bool_t cutsceneAudioStopUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneAudioStopLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioStopChannel);
-}
diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h
deleted file mode 100644
index 1423a98d..00000000
--- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "audio/mixer/audiomixerchannel.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts an audio stop step (stops the channel immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneAudioStopStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an audio stop step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneAudioStopUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an AUDIO_STOP cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneAudioStopLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt
deleted file mode 100644
index 55c2fdd3..00000000
--- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenebattlestart.c
- cutscenebattlesetstate.c
- cutscenebattlewaitselection.c
- cutscenebattlepostmove.c
- cutscenebattlefighterdamage.c
- cutscenebattlefighterheal.c
-)
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c
deleted file mode 100644
index 36a9bdbf..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "assert/assert.h"
-#include "util/string.h"
-
-#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
-
-void cutsceneBattleFighterDamageStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- const cutscenebattlefighterdamage_t *damage = &item->battleFighterDamage;
- assertTrue(
- damage->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
- "fighterIndex sentinel was never resolved"
- );
- battlefighter_t *fighter = BATTLE.fighters[damage->fighterIndex];
- assertNotNull(fighter, "Fighter cannot be NULL");
-
- battleFighterDamage(fighter, damage->amount);
-}
-
-bool_t cutsceneBattleFighterDamageUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneBattleFighterDamageLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
- if(!cutsceneJsonCopyString(
- itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
- )) {
- errorThrow("Cutscene item (BATTLE_FIGHTER_DAMAGE): missing 'fighter'");
- }
- if(stringEquals(fighterRef, "target")) {
- item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
- } else if(stringEquals(fighterRef, "user")) {
- item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_USER;
- } else {
- errorThrow(
- "Cutscene item (BATTLE_FIGHTER_DAMAGE): 'fighter' must be "
- "'target' or 'user'"
- );
- }
-
- const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
- if(amount < 0 || amount > UINT16_MAX) {
- errorThrow(
- "Cutscene item (BATTLE_FIGHTER_DAMAGE): invalid/missing 'amount'"
- );
- }
- item->battleFighterDamage.amount = (uint16_t)amount;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h
deleted file mode 100644
index 7d592732..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-
-typedef struct {
- // Index into BATTLE.fighters of the fighter to damage, or one of
- // BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
- // the currently-executing action by battleStateExecutingInit before
- // this item ever runs.
- battlefighterpos_t fighterIndex;
- uint16_t amount;
-} cutscenebattlefighterdamage_t;
-
-/**
- * Starts a BATTLE_FIGHTER_DAMAGE item, applying the configured damage to
- * BATTLE.fighters[fighterIndex] immediately.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneBattleFighterDamageStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a BATTLE_FIGHTER_DAMAGE item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneBattleFighterDamageUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a BATTLE_FIGHTER_DAMAGE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneBattleFighterDamageLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c
deleted file mode 100644
index ee288541..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "assert/assert.h"
-#include "util/string.h"
-
-#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16
-
-void cutsceneBattleFighterHealStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- const cutscenebattlefighterheal_t *heal = &item->battleFighterHeal;
- assertTrue(
- heal->fighterIndex < BATTLE_FIGHTER_POS_COUNT,
- "fighterIndex sentinel was never resolved"
- );
- battlefighter_t *fighter = BATTLE.fighters[heal->fighterIndex];
- assertNotNull(fighter, "Fighter cannot be NULL");
-
- battleFighterHeal(fighter, heal->amount);
-}
-
-bool_t cutsceneBattleFighterHealUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneBattleFighterHealLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX];
- if(!cutsceneJsonCopyString(
- itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX
- )) {
- errorThrow("Cutscene item (BATTLE_FIGHTER_HEAL): missing 'fighter'");
- }
- if(stringEquals(fighterRef, "target")) {
- item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_TARGET;
- } else if(stringEquals(fighterRef, "user")) {
- item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_USER;
- } else {
- errorThrow(
- "Cutscene item (BATTLE_FIGHTER_HEAL): 'fighter' must be "
- "'target' or 'user'"
- );
- }
-
- const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1);
- if(amount < 0 || amount > UINT16_MAX) {
- errorThrow(
- "Cutscene item (BATTLE_FIGHTER_HEAL): invalid/missing 'amount'"
- );
- }
- item->battleFighterHeal.amount = (uint16_t)amount;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h
deleted file mode 100644
index ae414b9d..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/battle/fighter/battlefighterpos.h"
-
-typedef struct {
- // Index into BATTLE.fighters of the fighter to heal, or one of
- // BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against
- // the currently-executing action by battleStateExecutingInit before
- // this item ever runs.
- battlefighterpos_t fighterIndex;
- uint16_t amount;
-} cutscenebattlefighterheal_t;
-
-/**
- * Starts a BATTLE_FIGHTER_HEAL item, applying the configured heal to
- * BATTLE.fighters[fighterIndex] immediately.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneBattleFighterHealStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a BATTLE_FIGHTER_HEAL item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneBattleFighterHealUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a BATTLE_FIGHTER_HEAL cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneBattleFighterHealLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c
deleted file mode 100644
index ba7e1b26..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "console/console.h"
-
-void cutsceneBattlePostMoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- consolePrint("BATTLE_POST_MOVE\n");
-
- // Once a result has already been decided (e.g. the player fled), there
- // is nothing left to check.
- if(BATTLE.result != BATTLE_RESULT_NONE) return;
-
- bool_t allyAlive = false;
- bool_t enemyAlive = false;
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- battlefighter_t *fighter = BATTLE.fighters[i];
- if(fighter == NULL) continue;
- if(!battleFighterIsAlive(fighter)) continue;
-
- if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) allyAlive = true;
- else enemyAlive = true;
- }
-
- if(allyAlive && enemyAlive) return;
-
- BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS;
-
- // Drop the rest of this round's already-spliced items (other fighters'
- // still-pending move cutscenes, the trailing BATTLE_SET_STATE(PRE_ROUND) -
- // see battleStateExecutingInit) - the battle is over, so none of that
- // should run against the WON/LOST/ENDING transition about to replace it.
- cutsceneSystemClearAfter(CUTSCENE_SYSTEM.currentItem);
-
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = {
- .state = allyAlive ? BATTLE_STATE_WON : BATTLE_STATE_LOST
- }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
-}
-
-bool_t cutsceneBattlePostMoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h
deleted file mode 100644
index 58b62943..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene
- * finishes: checks whether every ally or every enemy fighter has been
- * knocked out and, if so, sets BATTLE.result and splices in a
- * BATTLE_SET_STATE to BATTLE_STATE_WON/LOST. No-ops the health check if
- * BATTLE.result is already set (e.g. the player fled). Not
- * JSON-authorable -- constructed dynamically by battleStateExecutingInit.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneBattlePostMoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a BATTLE_POST_MOVE item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneBattlePostMoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c
deleted file mode 100644
index 5b715c4c..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/cutscene/item/json/cutscenejsonbattlestate.h"
-#include "assert/assert.h"
-
-void cutsceneBattleSetStateStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- assertNotNull(item, "item cannot be NULL");
- battleSetState(item->battleSetState.state);
-}
-
-bool_t cutsceneBattleSetStateUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneBattleSetStateLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- battlestate_t state;
- if(!cutsceneJsonLookupBattleState(
- yyjson_get_str(yyjson_obj_get(itemObj, "state")), &state
- )) {
- errorThrow("Cutscene item (BATTLE_SET_STATE): invalid 'state'");
- }
- item->battleSetState.state = state;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h
deleted file mode 100644
index 2d4753bf..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/state/battlestate.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- battlestate_t state;
-} cutscenebattlesetstate_t;
-
-/**
- * Starts a BATTLE_SET_STATE item, transitioning the battle to the
- * configured state immediately.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneBattleSetStateStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a BATTLE_SET_STATE item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneBattleSetStateUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a BATTLE_SET_STATE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneBattleSetStateLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c
deleted file mode 100644
index d136db47..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/battle/party.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-
-battlefighter_t CUTSCENE_BATTLE_START_ENEMY_POOL[
- CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
-];
-
-void cutsceneBattleStartStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- assertNotNull(item, "item cannot be NULL");
-
- const cutscenebattlestart_t *config = &item->battleStart;
- assertTrue(
- config->enemyCount <= CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX,
- "enemyCount out of bounds"
- );
-
- battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] = { 0 };
-
- memoryZero(
- CUTSCENE_BATTLE_START_ENEMY_POOL,
- sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL)
- );
- for(uint8_t i = 0; i < config->enemyCount; i++) {
- battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i];
- *enemy = config->enemies[i].fighter;
- fighters[i] = enemy;
- }
-
- for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
- battlefighter_t *member = partyGetOrderMember(i);
- if(member == NULL) continue;
-
- fighters[BATTLE_FIGHTER_POS_4 + i] = member;
- }
-
- battleStart(fighters);
-}
-
-bool_t cutsceneBattleStartUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneBattleStartLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *enemies = yyjson_obj_get(itemObj, "enemies");
- size_t enemyCount = yyjson_arr_size(enemies);
- if(
- !yyjson_is_arr(enemies) || enemyCount == 0 ||
- enemyCount > CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
- ) {
- errorThrow(
- "Cutscene item (BATTLE_START): 'enemies' must have 1-%u entries",
- (uint32_t)CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX
- );
- }
- item->battleStart.enemyCount = (uint8_t)enemyCount;
-
- yyjson_arr_iter iter = yyjson_arr_iter_with(enemies);
- yyjson_val *enemyVal;
- size_t e = 0;
- while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) {
- cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e];
-
- errorret_t parseResult = battleFighterParse(&enemy->fighter, enemyVal);
- if(errorIsNotOk(parseResult)) errorChain(parseResult);
-
- // Enemy JSON never specifies team/controller (battleFighterParse just
- // defaulted them to ALLY/PLAYER) - BATTLE_START enemies are always
- // ENEMY/AI.
- enemy->fighter.team = BATTLE_FIGHTER_TEAM_ENEMY;
- enemy->fighter.controller = BATTLE_FIGHTER_CONTROLLER_AI;
-
- e++;
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h
deleted file mode 100644
index c6024903..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/fighter/battlefighter.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-// Enemies always occupy the left-side positions (BATTLE_FIGHTER_POS_0-3).
-#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4
-
-typedef struct {
- // Fully parsed at Load time via battleFighterParse (team/controller
- // forced to ENEMY/AI right after, since enemy JSON only ever supplies
- // stats/abilities) - Start splices this directly into
- // CUTSCENE_BATTLE_START_ENEMY_POOL, no further init needed.
- battlefighter_t fighter;
-} cutscenebattlestartenemy_t;
-
-typedef struct {
- cutscenebattlestartenemy_t enemies[CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX];
- uint8_t enemyCount;
-} cutscenebattlestart_t;
-
-/**
- * Starts a BATTLE_START item: spawns the configured enemies, fills the
- * remaining positions with the active party (via partyGetOrderMember), and
- * calls battleStart. Always completes immediately - the battle scene takes
- * over from here.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneBattleStartStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a BATTLE_START item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneBattleStartUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a BATTLE_START cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneBattleStartLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c
deleted file mode 100644
index 763dbbff..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/battle/battle.h"
-#include "rpg/battle/state/battlestateselection.h"
-
-bool_t cutsceneBattleWaitSelectionUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- if(BATTLE.state != BATTLE_STATE_SELECTION) return true;
- if(!battleStateSelectionIsComplete()) return false;
-
- // Every fighter has decided - queue the move to BATTLE_STATE_EXECUTING.
- const cutsceneitem_t items[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- .battleSetState = { .state = BATTLE_STATE_EXECUTING }
- }
- };
- cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h
deleted file mode 100644
index c5235734..00000000
--- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Updates a BATTLE_WAIT_SELECTION step, completing once every fighter has
- * decided a move this round (letting the queued BATTLE_SET_STATE step
- * that follows move the battle on to BATTLE_STATE_EXECUTING) or once the
- * selecting fighter has fled (moving on to BATTLE_STATE_FLED). Has no
- * Start callback -- there's nothing to do until selection actually
- * finishes.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once selection has completed or the battle has left
- * BATTLE_STATE_SELECTION.
- */
-bool_t cutsceneBattleWaitSelectionUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt
deleted file mode 100644
index 43f8e9dd..00000000
--- a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenewait.c
- cutscenesetpause.c
- cutsceneconcurrent.c
- cutscenemarker.c
- cutscenerestart.c
- cutscenequitgame.c
- cutscenescene.c
- cutsceneidle.c
-)
diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c b/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c
deleted file mode 100644
index 7145b383..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "assert/assert.h"
-
-void cutsceneConcurrentStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- assertTrue(
- item->concurrent.count <= CUTSCENE_CONCURRENT_MAX,
- "Too many items in CUTSCENE_CONCURRENT"
- );
- for(uint8_t i = 0; i < item->concurrent.count; i++) {
- assertTrue(
- item->concurrent.items[i].type != CUTSCENE_ITEM_TYPE_CONCURRENT,
- "Concurrent items cannot be nested"
- );
- cutsceneItemStart(
- &item->concurrent.items[i],
- (cutsceneitemdata_t *)&data->concurrent.childData[i]
- );
- }
-}
-
-bool_t cutsceneConcurrentUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- for(uint8_t i = 0; i < item->concurrent.count; i++) {
- if(data->concurrent.doneMask & (1u << i)) continue;
- if(cutsceneItemUpdate(
- &item->concurrent.items[i],
- (cutsceneitemdata_t *)&data->concurrent.childData[i]
- )) {
- data->concurrent.doneMask |= (uint8_t)(1u << i);
- }
- }
- uint8_t allDone = (uint8_t)((1u << item->concurrent.count) - 1u);
- return data->concurrent.doneMask == allDone;
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h b/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h
deleted file mode 100644
index 71b0ba1e..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "cutscenewait.h"
-#include "rpg/cutscene/item/entity/cutsceneentitywalkto.h"
-
-/** Maximum number of items that may run inside a CUTSCENE_CONCURRENT. */
-#define CUTSCENE_CONCURRENT_MAX 8
-
-/**
- * Static (const) data for a concurrent cutscene item.
- */
-typedef struct {
- const cutsceneitem_t *items;
- uint8_t count;
-} cutsceneconcurrent_t;
-
-/**
- * Runtime data for one non-concurrent child item.
- * Concurrent items cannot be nested.
- */
-typedef union {
- cutscenewaitdata_t wait;
- cutsceneentitywalktodata_t entityWalkTo;
-} cutsceneconcurrentchilddata_t;
-
-/** Runtime data for a running concurrent item. */
-typedef struct {
- cutsceneconcurrentchilddata_t childData[CUTSCENE_CONCURRENT_MAX];
- uint8_t doneMask;
-} cutsceneconcurrentdata_t;
-
-/**
- * Starts a concurrent item (starts all child items simultaneously).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneConcurrentStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a concurrent item (ticks all unfinished children).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once every child has completed.
- */
-bool_t cutsceneConcurrentUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneidle.c b/src/dusk/rpg/cutscene/item/control/cutsceneidle.c
deleted file mode 100644
index fcadce3e..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutsceneidle.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-
-bool_t cutsceneIdleUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneidle.h b/src/dusk/rpg/cutscene/item/control/cutsceneidle.h
deleted file mode 100644
index b230e186..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutsceneidle.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Updates an idle item. Never completes on its own - the cutscene blocks
- * here indefinitely until something external (e.g. a UI callback) calls
- * cutsceneSystemGoTo to jump elsewhere.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneIdleUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-// IDLE has no fields of its own to parse - see cutsceneItemLoadNoop
-// (cutsceneitembase.h), used directly as its .load callback.
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.c b/src/dusk/rpg/cutscene/item/control/cutscenemarker.c
deleted file mode 100644
index 0c210f5c..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenemarker.c
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneMarkerStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
-}
-
-bool_t cutsceneMarkerUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneMarkerLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonCopyString(
- itemObj, "name", NULL, item->marker.name, CUTSCENE_MARKER_NAME_MAX
- )) {
- errorThrow("Cutscene item (MARKER): missing/too-long 'name'");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.h b/src/dusk/rpg/cutscene/item/control/cutscenemarker.h
deleted file mode 100644
index 0dc10e16..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenemarker.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-// Also the max length of a marker name referenced (not declared) by other
-// item types - see cutscenesavedevicecheck_t/cutscenesaveloadallslots_t/
-// cutscenemodaloptionsmarkers_t.
-#define CUTSCENE_MARKER_NAME_MAX 32
-
-typedef struct {
- char_t name[CUTSCENE_MARKER_NAME_MAX];
-} cutscenemarker_t;
-
-/**
- * Starts a marker item. A marker does nothing on its own - it exists
- * purely as a named position for cutsceneSystemGoTo to jump to.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneMarkerStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a marker item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneMarkerUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a MARKER cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneMarkerLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c
deleted file mode 100644
index b07a5e8e..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "engine/engine.h"
-
-void cutsceneQuitGameStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- ENGINE.running = false;
-}
-
-bool_t cutsceneQuitGameUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h
deleted file mode 100644
index 9848a84f..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts a quit-game item (sets ENGINE.running to false, ending the
- * process's main loop).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneQuitGameStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a quit-game item. The engine is shutting down by the time this
- * would run, so this always reports incomplete (mirrors
- * cutsceneRestartUpdate).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneQuitGameUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-// QUIT_GAME has no fields of its own to parse - see cutsceneItemLoadNoop
-// (cutsceneitembase.h), used directly as its .load callback.
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.c b/src/dusk/rpg/cutscene/item/control/cutscenerestart.c
deleted file mode 100644
index 160a35ba..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenerestart.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void cutsceneRestartStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- cutsceneRestart();
-}
-
-bool_t cutsceneRestartUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.h b/src/dusk/rpg/cutscene/item/control/cutscenerestart.h
deleted file mode 100644
index a1e05dd4..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenerestart.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts a restart item (restarts the currently running cutscene from
- * its first item via cutsceneRestart).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneRestartStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a restart item. By the time this would run, the cutscene has
- * already restarted from its first item, so this always reports
- * incomplete (mirrors cutsceneCutsceneUpdate).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneRestartUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-// RESTART has no fields of its own to parse - see cutsceneItemLoadNoop
-// (cutsceneitembase.h), used directly as its .load callback.
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.c b/src/dusk/rpg/cutscene/item/control/cutscenescene.c
deleted file mode 100644
index 57c92249..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenescene.c
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "scene/scene.h"
-#include "rpg/cutscene/item/json/cutscenejsonscenetype.h"
-
-void cutsceneSceneStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- sceneSet(item->sceneChange.type);
-}
-
-bool_t cutsceneSceneUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneSceneLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- scenetype_t sceneValue;
- if(!cutsceneJsonLookupSceneType(
- yyjson_get_str(yyjson_obj_get(itemObj, "sceneType")), &sceneValue
- )) {
- errorThrow("Cutscene item (SCENE): invalid 'sceneType'");
- }
- item->sceneChange.type = sceneValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.h b/src/dusk/rpg/cutscene/item/control/cutscenescene.h
deleted file mode 100644
index f3e2d23b..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenescene.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "scene/scenetype.h"
-
-typedef struct {
- scenetype_t type;
-} cutscenescene_t;
-
-/**
- * Starts a scene item (requests a switch to the given scene via
- * sceneSet). The switch itself doesn't happen until the next
- * sceneUpdate() tick, so the rest of the current frame - including
- * whatever else this cutscene does after this item - still runs
- * against the scene that's being left.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneSceneStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a scene item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneSceneUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a SCENE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneSceneLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c b/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c
deleted file mode 100644
index e434a73d..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/item/json/cutscenejsonpauseflags.h"
-
-void cutsceneSetPauseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- CUTSCENE_SYSTEM.pause = item->setPause;
-}
-
-bool_t cutsceneSetPauseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneSetPauseLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *flags = yyjson_obj_get(itemObj, "flags");
- cutscenepause_t value;
- if(!cutsceneJsonParsePauseFlags(flags, &value)) {
- errorThrow(
- "Cutscene item (SET_PAUSE): 'flags' must be an array of valid pause "
- "flags"
- );
- }
- item->setPause = value;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h b/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h
deleted file mode 100644
index 8636a50f..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/cutscenepause.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts a set-pause item (applies the new pause flags immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneSetPauseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a set-pause item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneSetPauseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a SET_PAUSE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneSetPauseLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenewait.c b/src/dusk/rpg/cutscene/item/control/cutscenewait.c
deleted file mode 100644
index be5b84f6..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenewait.c
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "time/time.h"
-
-void cutsceneWaitStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- data->wait = item->wait;
-}
-
-bool_t cutsceneWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- data->wait -= TIME.delta;
- return data->wait <= 0;
-}
-
-errorret_t cutsceneWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- item->wait = cutsceneJsonGetFloat(itemObj, "seconds", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/control/cutscenewait.h b/src/dusk/rpg/cutscene/item/control/cutscenewait.h
deleted file mode 100644
index 64ccd7cb..00000000
--- a/src/dusk/rpg/cutscene/item/control/cutscenewait.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef float_t cutscenewait_t;
-typedef float_t cutscenewaitdata_t;
-
-/**
- * Starts a wait item (stores the duration in data).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneWaitStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a wait item.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true when the wait has elapsed.
- */
-bool_t cutsceneWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a WAIT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/cutscene/item/cutscenecallback.c b/src/dusk/rpg/cutscene/item/cutscenecallback.c
deleted file mode 100644
index 9f5c0743..00000000
--- a/src/dusk/rpg/cutscene/item/cutscenecallback.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void cutsceneCallbackStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- if(item->callback != NULL) item->callback(NULL);
-}
-
-bool_t cutsceneCallbackUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/cutscenecallback.h b/src/dusk/rpg/cutscene/item/cutscenecallback.h
deleted file mode 100644
index 41029607..00000000
--- a/src/dusk/rpg/cutscene/item/cutscenecallback.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef void (*cutscenecallback_t)(void *userData);
-
-/**
- * Starts a callback item (invokes the callback immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneCallbackStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a callback item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneCallbackUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c
deleted file mode 100644
index 558b810a..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneitem.c
+++ /dev/null
@@ -1,562 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "ui/overlay/uifatalerror.h"
-
-cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
- [CUTSCENE_ITEM_TYPE_NULL] = { 0 },
-
- // Control
- [CUTSCENE_ITEM_TYPE_CALLBACK] = {
- .init = cutsceneCallbackStart,
- .update = cutsceneCallbackUpdate
- },
-
- [CUTSCENE_ITEM_TYPE_WAIT] = {
- .init = cutsceneWaitStart,
- .update = cutsceneWaitUpdate,
- .jsonName = "WAIT",
- .load = cutsceneWaitLoad
- },
-
- [CUTSCENE_ITEM_TYPE_CUTSCENE] = {
- .init = cutsceneCutsceneStart,
- .update = cutsceneCutsceneUpdate,
- .jsonName = "CUTSCENE",
- .load = cutsceneCutsceneLoad
- },
-
- [CUTSCENE_ITEM_TYPE_INSERT] = {
- .init = cutsceneInsertStart,
- .update = cutsceneInsertUpdate,
- .jsonName = "INSERT",
- .load = cutsceneInsertLoad
- },
-
- [CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
- .init = cutsceneSetPauseStart,
- .update = cutsceneSetPauseUpdate,
- .jsonName = "SET_PAUSE",
- .load = cutsceneSetPauseLoad
- },
-
- [CUTSCENE_ITEM_TYPE_CONCURRENT] = {
- .init = cutsceneConcurrentStart,
- .update = cutsceneConcurrentUpdate
- },
-
- [CUTSCENE_ITEM_TYPE_MARKER] = {
- .init = cutsceneMarkerStart,
- .update = cutsceneMarkerUpdate,
- .jsonName = "MARKER",
- .load = cutsceneMarkerLoad
- },
-
- [CUTSCENE_ITEM_TYPE_RESTART] = {
- .init = cutsceneRestartStart,
- .update = cutsceneRestartUpdate,
- .jsonName = "RESTART",
- .load = cutsceneItemLoadNoop
- },
-
- [CUTSCENE_ITEM_TYPE_QUIT_GAME] = {
- .init = cutsceneQuitGameStart,
- .update = cutsceneQuitGameUpdate,
- .jsonName = "QUIT_GAME",
- .load = cutsceneItemLoadNoop
- },
-
- [CUTSCENE_ITEM_TYPE_SCENE] = {
- .init = cutsceneSceneStart,
- .update = cutsceneSceneUpdate,
- .jsonName = "SCENE",
- .load = cutsceneSceneLoad
- },
-
- [CUTSCENE_ITEM_TYPE_IDLE] = {
- .update = cutsceneIdleUpdate,
- .jsonName = "IDLE",
- .load = cutsceneItemLoadNoop
- },
-
- // Entity
- [CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
- .init = cutsceneEntityTeleportStart,
- .update = cutsceneEntityTeleportUpdate,
- .jsonName = "ENTITY_TELEPORT",
- .load = cutsceneEntityTeleportLoad
- },
-
- [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
- .init = cutsceneEntityWalkToStart,
- .update = cutsceneEntityWalkToUpdate,
- .jsonName = "ENTITY_WALK_TO",
- .load = cutsceneEntityWalkToLoad
- },
-
- [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
- .init = cutsceneEntityWalkToEntityStart,
- .update = cutsceneEntityWalkToEntityUpdate,
- .jsonName = "ENTITY_WALK_TO_ENTITY",
- .load = cutsceneEntityWalkToEntityLoad
- },
-
- [CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
- .init = cutsceneEntityRemoveStart,
- .update = cutsceneEntityRemoveUpdate,
- .jsonName = "ENTITY_REMOVE",
- .load = cutsceneEntityRemoveLoad
- },
-
- [CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
- .init = cutsceneEntityAddStart,
- .update = cutsceneEntityAddUpdate,
- .jsonName = "ENTITY_ADD",
- .load = cutsceneEntityAddLoad
- },
-
- [CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
- .init = cutsceneEntityTurnStart,
- .update = cutsceneEntityTurnUpdate,
- .jsonName = "ENTITY_TURN",
- .load = cutsceneEntityTurnLoad
- },
-
- // UI
- [CUTSCENE_ITEM_TYPE_TEXT] = {
- .init = cutsceneTextStart,
- .update = cutsceneTextUpdate,
- .jsonName = "TEXT",
- .load = cutsceneTextLoad
- },
-
- [CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
- .init = cutsceneTextMiniStart,
- .update = cutsceneTextMiniUpdate,
- .jsonName = "TEXT_MINI",
- .load = cutsceneTextMiniLoad
- },
-
- [CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
- .init = cutsceneTextMiniHideStart,
- .update = cutsceneTextMiniHideUpdate,
- .jsonName = "TEXT_MINI_HIDE",
- .load = cutsceneTextMiniHideLoad
- },
-
- [CUTSCENE_ITEM_TYPE_PRINT] = {
- .init = cutscenePrintStart,
- .update = cutscenePrintUpdate,
- .jsonName = "PRINT",
- .load = cutscenePrintLoad
- },
-
- [CUTSCENE_ITEM_TYPE_FADE] = {
- .init = cutsceneFadeStart,
- .update = cutsceneFadeUpdate,
- .jsonName = "FADE",
- .load = cutsceneFadeLoad
- },
-
- [CUTSCENE_ITEM_TYPE_EMOJI] = {
- .init = cutsceneEmojiStart,
- .update = cutsceneEmojiUpdate,
- .jsonName = "EMOJI",
- .load = cutsceneEmojiLoad
- },
-
- [CUTSCENE_ITEM_TYPE_SHAKE] = {
- .init = cutsceneShakeStart,
- .update = cutsceneShakeUpdate,
- .jsonName = "SHAKE",
- .load = cutsceneShakeLoad
- },
-
- [CUTSCENE_ITEM_TYPE_MODAL] = {
- .init = cutsceneModalStart,
- .update = cutsceneModalUpdate,
- .jsonName = "MODAL",
- .load = cutsceneModalLoad
- },
-
- [CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
- .init = cutsceneModalOptionsMarkersStart,
- .update = cutsceneModalOptionsMarkersUpdate,
- .jsonName = "MODAL_OPTIONS_MARKERS",
- .load = cutsceneModalOptionsMarkersLoad
- },
-
- [CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
- .init = cutsceneModalCloseStart,
- .update = cutsceneModalCloseUpdate,
- .jsonName = "MODAL_CLOSE",
- .load = cutsceneItemLoadNoop
- },
-
- [CUTSCENE_ITEM_TYPE_KEYBOARD] = {
- .init = cutsceneKeyboardStart,
- .update = cutsceneKeyboardUpdate
- },
-
- [CUTSCENE_ITEM_TYPE_UI_SHOW] = {
- .init = cutsceneUIShowStart,
- .update = cutsceneUIShowUpdate,
- .jsonName = "UI_SHOW",
- .load = cutsceneUIShowLoad
- },
-
- // Item
- [CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
- .init = cutsceneItemGiveStart,
- .update = cutsceneItemGiveUpdate,
- .jsonName = "ITEM_GIVE",
- .load = cutsceneItemGiveLoad
- },
-
- // Map area
- [CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
- .init = cutsceneMapAreaAddStart,
- .update = cutsceneMapAreaAddUpdate
- },
-
- [CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
- .init = cutsceneMapAreaRemoveStart,
- .update = cutsceneMapAreaRemoveUpdate,
- .jsonName = "MAP_AREA_REMOVE",
- .load = cutsceneMapAreaRemoveLoad
- },
-
- [CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
- .init = cutsceneMapAreaWaitStart,
- .update = cutsceneMapAreaWaitUpdate,
- .jsonName = "MAP_AREA_WAIT",
- .load = cutsceneMapAreaWaitLoad
- },
-
- // Battle
- [CUTSCENE_ITEM_TYPE_BATTLE_START] = {
- .init = cutsceneBattleStartStart,
- .update = cutsceneBattleStartUpdate,
- .jsonName = "BATTLE_START",
- .load = cutsceneBattleStartLoad
- },
-
- [CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE] = {
- .init = cutsceneBattleSetStateStart,
- .update = cutsceneBattleSetStateUpdate,
- .jsonName = "BATTLE_SET_STATE",
- .load = cutsceneBattleSetStateLoad
- },
-
- [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION] = {
- .update = cutsceneBattleWaitSelectionUpdate,
- .jsonName = "BATTLE_WAIT_SELECTION",
- .load = cutsceneItemLoadNoop
- },
-
- [CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = {
- .init = cutsceneBattlePostMoveStart,
- .update = cutsceneBattlePostMoveUpdate
- },
-
- [CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE] = {
- .init = cutsceneBattleFighterDamageStart,
- .update = cutsceneBattleFighterDamageUpdate,
- .jsonName = "BATTLE_FIGHTER_DAMAGE",
- .load = cutsceneBattleFighterDamageLoad
- },
-
- [CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL] = {
- .init = cutsceneBattleFighterHealStart,
- .update = cutsceneBattleFighterHealUpdate,
- .jsonName = "BATTLE_FIGHTER_HEAL",
- .load = cutsceneBattleFighterHealLoad
- },
-
- // Save
- [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
- .init = cutsceneSaveDeviceCheckStart,
- .update = cutsceneSaveDeviceCheckUpdate,
- .jsonName = "SAVE_DEVICE_CHECK",
- .load = cutsceneSaveDeviceCheckLoad
- },
-
- [CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = {
- .init = cutsceneSaveLoadAllSlotsStart,
- .update = cutsceneSaveLoadAllSlotsUpdate,
- .jsonName = "SAVE_LOAD_ALL_SLOTS",
- .load = cutsceneSaveLoadAllSlotsLoad
- },
-
- // Audio
- [CUTSCENE_ITEM_TYPE_AUDIO_PLAY] = {
- .init = cutsceneAudioPlayStart,
- .update = cutsceneAudioPlayUpdate,
- .jsonName = "AUDIO_PLAY",
- .load = cutsceneAudioPlayLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_STOP] = {
- .init = cutsceneAudioStopStart,
- .update = cutsceneAudioStopUpdate,
- .jsonName = "AUDIO_STOP",
- .load = cutsceneAudioStopLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_PAUSE] = {
- .init = cutsceneAudioPauseStart,
- .update = cutsceneAudioPauseUpdate,
- .jsonName = "AUDIO_PAUSE",
- .load = cutsceneAudioPauseLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_RESUME] = {
- .init = cutsceneAudioResumeStart,
- .update = cutsceneAudioResumeUpdate,
- .jsonName = "AUDIO_RESUME",
- .load = cutsceneAudioResumeLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_FADE] = {
- .init = cutsceneAudioFadeStart,
- .update = cutsceneAudioFadeUpdate,
- .jsonName = "AUDIO_FADE",
- .load = cutsceneAudioFadeLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT] = {
- .update = cutsceneAudioFadeWaitUpdate,
- .jsonName = "AUDIO_FADE_WAIT",
- .load = cutsceneAudioFadeWaitLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN] = {
- .init = cutsceneAudioSetPanStart,
- .update = cutsceneAudioSetPanUpdate,
- .jsonName = "AUDIO_SET_PAN",
- .load = cutsceneAudioSetPanLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP] = {
- .init = cutsceneAudioSetLoopStart,
- .update = cutsceneAudioSetLoopUpdate,
- .jsonName = "AUDIO_SET_LOOP",
- .load = cutsceneAudioSetLoopLoad
- },
-
- [CUTSCENE_ITEM_TYPE_AUDIO_SET] = {
- .init = cutsceneAudioSetStart,
- .update = cutsceneAudioSetUpdate,
- .jsonName = "AUDIO_SET",
- .load = cutsceneAudioSetLoad
- },
-
- // Story
- [CUTSCENE_ITEM_TYPE_STORY_SET_FLAG] = {
- .init = cutsceneStorySetFlagStart,
- .update = cutsceneStorySetFlagUpdate,
- .jsonName = "STORY_SET_FLAG",
- .load = cutsceneStorySetFlagLoad
- },
-
- [CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG] = {
- .update = cutsceneStoryWaitFlagUpdate,
- .jsonName = "STORY_WAIT_FLAG",
- .load = cutsceneStoryWaitFlagLoad
- },
-
- // Party
- [CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER] = {
- .init = cutscenePartyAddMemberStart,
- .update = cutscenePartyAddMemberUpdate,
- .jsonName = "PARTY_ADD_MEMBER",
- .load = cutscenePartyAddMemberLoad
- },
-
- [CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER] = {
- .init = cutscenePartyRemoveMemberStart,
- .update = cutscenePartyRemoveMemberUpdate,
- .jsonName = "PARTY_REMOVE_MEMBER",
- .load = cutscenePartyRemoveMemberLoad
- },
-
- [CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER] = {
- .init = cutscenePartyHealMemberStart,
- .update = cutscenePartyHealMemberUpdate,
- .jsonName = "PARTY_HEAL_MEMBER",
- .load = cutscenePartyHealMemberLoad
- },
-
- [CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL] = {
- .init = cutscenePartyHealAllStart,
- .update = cutscenePartyHealAllUpdate,
- .jsonName = "PARTY_HEAL_ALL",
- .load = cutsceneItemLoadNoop
- }
-};
-
-bool_t cutsceneItemLookupType(
- const char_t *name,
- cutsceneitemtype_t *outType
-) {
- if(name == NULL) return false;
- for(size_t i = 0; i < CUTSCENE_ITEM_TYPE_COUNT; i++) {
- const char_t *jsonName = CUTSCENE_ITEM_CALLBACKS[i].jsonName;
- if(jsonName != NULL && stringEquals(jsonName, name)) {
- *outType = (cutsceneitemtype_t)i;
- return true;
- }
- }
- return false;
-}
-
-void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
- cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
- if(init != NULL) init(item, data);
-}
-
-bool_t cutsceneItemUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- cutsceneitemupdatecallback_t *update =
- CUTSCENE_ITEM_CALLBACKS[item->type].update;
- if(update == NULL) return false;
-
- return update(item, data);
-}
-
-void cutsceneCutsceneStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- cutscene_t *cutscene = item->cutsceneRef.cutscene;
- if(cutscene == NULL && item->cutsceneRef.name[0] != '\0') {
- // Not cached back into item - with only one shared, fixed load buffer
- // (see cutscenesystem.h's CUTSCENE_LOADED_ITEMS_MAX doc comment),
- // every JSON-authored reference resolves to the same fixed address
- // every time anyway, and item itself lives inside that very buffer -
- // caching a pointer into it here would go stale (or, worse, get
- // clobbered mid-resolve) the moment a later load overwrites it.
- cutscene = cutsceneCutsceneResolve(item->cutsceneRef.name);
- }
- if(cutscene != NULL) cutsceneSystemStartCutscene(cutscene);
-}
-
-bool_t cutsceneCutsceneUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
-
-cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
- // Copy name out before it can be invalidated: this is commonly called
- // with a pointer into CUTSCENE_SYSTEM.loadedItems itself (a running
- // CUTSCENE/INSERT item's own cutsceneRef.name/insert.name field, when
- // the running cutscene IS loadedScene, e.g. initial.jsonc's closing
- // CUTSCENE item into main_menu.jsonc) - cutsceneLoadParse below parses
- // straight into that same shared loadedItems buffer, overwriting the
- // very memory name points into with the target cutscene's own first
- // item before this function is done reading name.
- char_t nameCopy[ASSET_FILE_NAME_MAX];
- stringCopy(nameCopy, name, ASSET_FILE_NAME_MAX - 1);
-
- errorret_t result = cutsceneLoadParse(
- nameCopy, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
- CUTSCENE_LOADED_ITEMS_MAX
- );
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(NULL);
- return NULL;
- }
-
- // Shares loadedScene/loadedItems with cutsceneSystemLoad, so track the
- // source the same way it does - cutsceneSystemGoTo needs this to reload the
- // untouched original when jumping to a marker behind the current
- // position (see cutsceneSystemGoTo's doc comment).
- stringCopy(CUTSCENE_SYSTEM.loadedFile, nameCopy, ASSET_FILE_NAME_MAX - 1);
-
- return &CUTSCENE_SYSTEM.loadedScene;
-}
-
-errorret_t cutsceneCutsceneLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneCutsceneRefLoad(itemObj, &item->cutsceneRef, "CUTSCENE");
-}
-
-errorret_t cutsceneCutsceneRefLoad(
- yyjson_val *itemObj,
- cutscenecutsceneref_t *outRef,
- const char_t *itemTypeLabel
-) {
- const char_t *name = yyjson_get_str(yyjson_obj_get(itemObj, "name"));
- if(name == NULL) {
- errorThrow("Cutscene item (%s): missing 'name'", itemTypeLabel);
- }
-
- // Only validate that name itself would fit - the reference itself is
- // not resolved/locked until this item actually runs (see
- // cutsceneCutsceneStart/cutsceneCutsceneResolve).
- int32_t nameLen = stringFormat(NULL, 0, "%s", name);
- if(nameLen < 0 || (size_t)nameLen >= CUTSCENE_CUTSCENE_REF_NAME_MAX) {
- errorThrow("Cutscene item (%s): 'name' too long", itemTypeLabel);
- }
-
- outRef->cutscene = NULL;
- stringCopy(outRef->name, name, CUTSCENE_CUTSCENE_REF_NAME_MAX);
-
- errorOk();
-}
-
-void cutsceneInsertStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- if(item->insert.cutscene != NULL) {
- cutsceneSystemInsertCutscene(item->insert.cutscene);
- } else if(item->insert.name[0] != '\0') {
- cutsceneInsertResolveAndSplice(item->insert.name);
- }
-}
-
-bool_t cutsceneInsertUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
-
-void cutsceneInsertResolveAndSplice(const char_t *name) {
- // Local scratch, not a shared buffer - this target is spliced straight
- // into the running scene below and never referenced again afterward, so
- // it only needs to live for the rest of this call.
- cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX];
- cutscene_t scene = { .items = items };
- errorret_t result = cutsceneLoadParse(name, &scene, items, CUTSCENE_INSERT_ITEMS_MAX);
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(NULL);
- return;
- }
-
- cutsceneSystemInsertCutscene(&scene);
-}
-
-errorret_t cutsceneInsertLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- return cutsceneCutsceneRefLoad(itemObj, &item->insert, "INSERT");
-}
diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h
deleted file mode 100644
index 84b09277..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneitem.h
+++ /dev/null
@@ -1,367 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "yyjson.h"
-#include "asset/assetfile.h"
-#include "cutsceneitembase.h"
-#include "cutscenecallback.h"
-#include "cutsceneprint.h"
-#include "control/cutscenewait.h"
-#include "control/cutscenesetpause.h"
-#include "control/cutsceneconcurrent.h"
-#include "control/cutscenemarker.h"
-#include "control/cutscenerestart.h"
-#include "control/cutscenequitgame.h"
-#include "control/cutscenescene.h"
-#include "control/cutsceneidle.h"
-#include "entity/cutsceneentityteleport.h"
-#include "entity/cutsceneentitywalkto.h"
-#include "entity/cutsceneentityremove.h"
-#include "entity/cutsceneentityadd.h"
-#include "entity/cutsceneentityturn.h"
-#include "entity/cutsceneentitywalktoentity.h"
-#include "ui/cutscenetext.h"
-#include "ui/cutscenetextmini.h"
-#include "ui/cutscenetextminihide.h"
-#include "ui/cutscenefade.h"
-#include "ui/cutsceneemoji.h"
-#include "ui/cutsceneshake.h"
-#include "ui/cutscenemodal.h"
-#include "ui/cutscenemodaloptionsmarkers.h"
-#include "ui/cutscenekeyboard.h"
-#include "ui/cutsceneuishow.h"
-#include "item/cutsceneitemgive.h"
-#include "maparea/cutscenemapareaadd.h"
-#include "maparea/cutscenemaparearemove.h"
-#include "maparea/cutscenemapareawait.h"
-#include "battle/cutscenebattlestart.h"
-#include "battle/cutscenebattlesetstate.h"
-#include "battle/cutscenebattlewaitselection.h"
-#include "battle/cutscenebattlepostmove.h"
-#include "battle/cutscenebattlefighterdamage.h"
-#include "battle/cutscenebattlefighterheal.h"
-#include "save/cutscenesavedevicecheck.h"
-#include "save/cutscenesaveloadallslots.h"
-#include "audio/cutsceneaudioplay.h"
-#include "audio/cutsceneaudiostop.h"
-#include "audio/cutsceneaudiopause.h"
-#include "audio/cutsceneaudioresume.h"
-#include "audio/cutsceneaudiofade.h"
-#include "audio/cutsceneaudiofadewait.h"
-#include "audio/cutsceneaudiosetpan.h"
-#include "audio/cutsceneaudiosetloop.h"
-#include "audio/cutsceneaudioset.h"
-#include "story/cutscenestorysetflag.h"
-#include "story/cutscenestorywaitflag.h"
-#include "party/cutscenepartyaddmember.h"
-#include "party/cutscenepartyremovemember.h"
-#include "party/cutscenepartyhealmember.h"
-#include "party/cutscenepartyhealall.h"
-
-typedef struct cutscene_s cutscene_t;
-
-// Max length of a CUTSCENE/INSERT item's "name" field - it's the full
-// asset path to load, so this is bounded by ASSET_FILE_NAME_MAX directly.
-#define CUTSCENE_CUTSCENE_REF_NAME_MAX ASSET_FILE_NAME_MAX
-
-// A CUTSCENE (or INSERT) item's target. A C-authored cutscene_t* set
-// directly (e.g. by a test) leaves name empty ("") - safe to hold onto
-// indefinitely, since C-authored cutscenes are compiled directly into
-// their own static arrays, never a shared load buffer. JSON-authored
-// cutscenes (cutsceneCutsceneLoad/cutsceneInsertLoad) do the opposite -
-// only name is set at parse time (copied by value out of the JSON doc,
-// so it stays valid regardless of the doc's own lifetime), since
-// resolving/locking the referenced asset there would eagerly load every
-// cutscene a cutscene merely references, even ones never played.
-// cutsceneCutsceneStart resolves name to cutscene the first time this
-// item actually runs (via cutsceneCutsceneResolve) but does NOT cache the
-// result back into this field - with only one shared, fixed buffer (see
-// CUTSCENE_LOADED_ITEMS_MAX in cutscenesystem.h), every JSON-authored
-// reference resolves to the same fixed address every time anyway, and
-// item itself may live inside that very buffer, so caching a pointer
-// into it here would go stale (or, worse, get clobbered mid-resolve) the
-// moment a later load overwrites it. cutsceneInsertStart instead resolves
-// AND splices name in one step (cutsceneInsertResolveAndSplice) using a
-// local scratch buffer that's gone by the time this item's Start
-// returns, so there's nothing to cache back for INSERT even in principle.
-// Shared by both CUTSCENE (item->cutsceneRef) and INSERT (item->insert),
-// since both need exactly this "resolved pointer, or a name to lazily
-// resolve" shape.
-typedef struct {
- cutscene_t *cutscene;
- char_t name[CUTSCENE_CUTSCENE_REF_NAME_MAX];
-} cutscenecutsceneref_t;
-
-struct cutsceneitem_s {
- cutsceneitemtype_t type;
-
- union {
- cutscenetext_t text;
- cutscenetextmini_t textMini;
- cutscenetextminihide_t textMiniHide;
- cutscenecallback_t callback;
- cutscenewait_t wait;
- cutscenecutsceneref_t cutsceneRef;
- cutscenecutsceneref_t insert;
- cutsceneentityteleport_t entityTeleport;
- cutsceneentitywalkto_t entityWalkTo;
- cutscenefade_t fade;
- cutscenepause_t setPause;
- cutsceneconcurrent_t concurrent;
- cutsceneitemgive_t itemGive;
- cutsceneentityremove_t entityRemove;
- cutsceneentityadd_t entityAdd;
- cutsceneentityturn_t entityTurn;
- cutsceneentitywalktoentity_t entityWalkToEntity;
- cutscenemapareaadd_t mapAreaAdd;
- cutscenemaparearemove_t mapAreaRemove;
- cutscenemapareawait_t mapAreaWait;
- cutscenebattlestart_t battleStart;
- cutscenebattlesetstate_t battleSetState;
- cutscenebattlefighterdamage_t battleFighterDamage;
- cutscenebattlefighterheal_t battleFighterHeal;
- cutsceneemoji_t emoji;
- cutsceneshake_t shake;
- cutscenemodal_t modal;
- cutscenemodaloptionsmarkers_t modalOptionsMarkers;
- cutsceneprint_t print;
- cutscenemarker_t marker;
- cutscenescene_t sceneChange;
- cutscenesavedevicecheck_t saveDeviceCheck;
- cutscenesaveloadallslots_t saveLoadAllSlots;
- cutscenekeyboard_t keyboard;
- cutsceneaudioplay_t audioPlay;
- audiomixerchannel_t audioStopChannel;
- audiomixerchannel_t audioPauseChannel;
- audiomixerchannel_t audioResumeChannel;
- cutsceneaudiofade_t audioFade;
- audiomixerchannel_t audioFadeWaitChannel;
- cutsceneaudiosetpan_t audioSetPan;
- cutsceneaudiosetloop_t audioSetLoop;
- cutsceneaudioset_t audioSet;
- cutsceneuishow_t uiShow;
- cutscenestorysetflag_t storySetFlag;
- cutscenestorywaitflag_t storyWaitFlag;
- cutscenepartyaddmember_t partyAddMember;
- cutscenepartyremovemember_t partyRemoveMember;
- cutscenepartyhealmember_t partyHealMember;
- };
-};
-
-typedef union cutsceneitemdata_u {
- cutscenewaitdata_t wait;
- cutsceneentitywalktodata_t entityWalkTo;
- cutsceneconcurrentdata_t concurrent;
- cutscenemapareawaitdata_t mapAreaWait;
-} cutsceneitemdata_t;
-
-typedef void (cutsceneiteminitcallback_t)(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-typedef bool_t (cutsceneitemupdatecallback_t)(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-typedef errorret_t (cutsceneitemloadcallback_t)(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
-
-typedef struct {
- cutsceneiteminitcallback_t *init;
- cutsceneitemupdatecallback_t *update;
- const char_t *jsonName; // NULL if not JSON-authorable
- cutsceneitemloadcallback_t *load; // NULL if not JSON-authorable
-} cutsceneitemcallbacks_t;
-
-extern cutsceneitemcallbacks_t
- CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
-
-/**
- * Start the given cutscene item.
- *
- * @param item The cutscene item to start.
- * @param data Runtime data storage (pre-zeroed by caller).
- */
-void cutsceneItemStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Tick the given cutscene item (one frame).
- *
- * @param item The cutscene item to tick.
- * @param data Runtime data storage.
- * @returns true if the item is complete and the cutscene should advance.
- */
-bool_t cutsceneItemUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Looks up name (exact case) against every JSON-authorable item type's
- * jsonName.
- *
- * @param name Name to search for.
- * @param outType Set to the matching item type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneItemLookupType(
- const char_t *name,
- cutsceneitemtype_t *outType
-);
-
-/**
- * Starts a nested-cutscene item, handing control over to the referenced
- * cutscene. If item->cutsceneRef.cutscene hasn't been resolved yet (a
- * JSON-authored reference, only known by name so far), resolves it first
- * via cutsceneCutsceneResolve (not cached back - see cutscenecutsceneref_t's
- * doc comment for why).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneCutsceneStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Resolves a JSON-authored CUTSCENE item's referenced cutscene by name:
- * locks+loads name as a plain ASSET_LOADER_TYPE_JSON asset, parses it into
- * CUTSCENE_SYSTEM.loadedItems/loadedScene, then unlocks the asset entry
- * like any other asset - the parsed items don't reference the doc past
- * that (see cutsceneInsertResolveAndSplice's matching comment for why).
- * Called lazily from cutsceneCutsceneStart the first time the item
- * actually runs, not at parse time, so a cutscene referencing many others
- * doesn't load every one of them up front. On failure, opens the fatal
- * error overlay (matching cutsceneSystemLoad's runtime-failure handling)
- * and returns NULL.
- *
- * @param name Full asset path to the referenced cutscene's JSON file.
- * @return The resolved cutscene, or NULL on failure.
- */
-cutscene_t * cutsceneCutsceneResolve(const char_t *name);
-
-/**
- * Updates a nested-cutscene item. By the time this would run, control
- * has already moved on to the referenced cutscene, so this always
- * reports incomplete.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneCutsceneUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a CUTSCENE cutscene item's JSON fields into item. Kept inline here
- * (rather than its own file) since it's tightly bound to the asset/loader
- * system itself. Only stores the 'name' string (which points into the
- * cutscene asset's still-alive yyjson doc, so no copy is needed) -
- * resolving it to the referenced asset is deferred to cutsceneCutsceneStart,
- * the first time this item actually runs, so a cutscene referencing many
- * others doesn't pay to load every one of them up front.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneCutsceneLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
-
-/**
- * Shared JSON parsing body for both CUTSCENE and INSERT items - both need
- * exactly a 'name' string field, the full asset path to the referenced
- * cutscene's JSON file, validated to fit CUTSCENE_CUTSCENE_REF_NAME_MAX.
- * Only stores the name (see cutsceneCutsceneLoad for why resolving is
- * deferred).
- *
- * @param itemObj The item's JSON object.
- * @param outRef Destination ref, already zeroed by the caller.
- * @param itemTypeLabel Item type name used only in the error message on
- * failure (e.g. "CUTSCENE" or "INSERT").
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneCutsceneRefLoad(
- yyjson_val *itemObj,
- cutscenecutsceneref_t *outRef,
- const char_t *itemTypeLabel
-);
-
-/**
- * Starts an INSERT item, splicing the referenced cutscene's items into the
- * running cutscene in place via cutsceneSystemInsertCutscene - unlike
- * CUTSCENE (a one-way jump that replaces the running cutscene outright),
- * whatever already followed this item continues normally once the
- * inserted items run out, just shifted further down the same array. If
- * item->insert.cutscene is set (a compile-time target), splices it
- * directly; otherwise (a JSON-authored reference, only known by name so
- * far) resolves and splices it via cutsceneInsertResolveAndSplice.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneInsertStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Resolves a JSON-authored INSERT item's referenced cutscene by name and
- * splices it into the running cutscene (CUTSCENE_SYSTEM.scene): locks+
- * loads name as a plain ASSET_LOADER_TYPE_JSON asset, parses it into a
- * small scratch buffer local to this call (capacity
- * CUTSCENE_INSERT_ITEMS_MAX) straight out of the asset entry's own doc,
- * then unlocks the entry like any other asset - nothing needs to keep the
- * doc itself resident past the parse: every cutscene item field that could
- * reference it owns its string data by value (see
- * cutscenecutsceneref_t/cutscenemarker_t's doc comments), and
- * cutsceneSystemInsertCutscene copies the scratch items into the running
- * scene's own array right after. On failure, opens the fatal error
- * overlay, matching cutsceneCutsceneResolve.
- *
- * @param name Full asset path to the referenced cutscene's JSON file.
- */
-void cutsceneInsertResolveAndSplice(const char_t *name);
-
-/**
- * Updates an INSERT item. By the time this would run, control has already
- * moved on to the inserted items, so this always reports incomplete.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneInsertUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an INSERT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneInsertLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.c b/src/dusk/rpg/cutscene/item/cutsceneitembase.c
deleted file mode 100644
index 11cb848e..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneitembase.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutsceneitembase.h"
-
-errorret_t cutsceneItemLoadNoop(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h
deleted file mode 100644
index b53bcbbf..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "error/error.h"
-#include "yyjson.h"
-
-typedef struct cutsceneitem_s cutsceneitem_t;
-typedef union cutsceneitemdata_u cutsceneitemdata_t;
-
-typedef enum {
- CUTSCENE_ITEM_TYPE_NULL,
-
- // Control
- CUTSCENE_ITEM_TYPE_CALLBACK,
- CUTSCENE_ITEM_TYPE_WAIT,
- CUTSCENE_ITEM_TYPE_CUTSCENE,
- CUTSCENE_ITEM_TYPE_INSERT,
- CUTSCENE_ITEM_TYPE_SET_PAUSE,
- CUTSCENE_ITEM_TYPE_CONCURRENT,
- CUTSCENE_ITEM_TYPE_MARKER,
- CUTSCENE_ITEM_TYPE_RESTART,
- CUTSCENE_ITEM_TYPE_QUIT_GAME,
- CUTSCENE_ITEM_TYPE_SCENE,
- CUTSCENE_ITEM_TYPE_IDLE,
-
- // Entity
- CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT,
- CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO,
- CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
- CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
- CUTSCENE_ITEM_TYPE_ENTITY_ADD,
- CUTSCENE_ITEM_TYPE_ENTITY_TURN,
-
- // UI
- CUTSCENE_ITEM_TYPE_TEXT,
- CUTSCENE_ITEM_TYPE_TEXT_MINI,
- CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
- CUTSCENE_ITEM_TYPE_PRINT,
- CUTSCENE_ITEM_TYPE_FADE,
- CUTSCENE_ITEM_TYPE_EMOJI,
- CUTSCENE_ITEM_TYPE_SHAKE,
- CUTSCENE_ITEM_TYPE_MODAL,
- CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
- CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
- CUTSCENE_ITEM_TYPE_KEYBOARD,
- CUTSCENE_ITEM_TYPE_UI_SHOW,
-
- // Item
- CUTSCENE_ITEM_TYPE_ITEM_GIVE,
-
- // Map area
- CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
- CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
- CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
-
- // Battle
- CUTSCENE_ITEM_TYPE_BATTLE_START,
- CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
- CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION,
- CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE,
- CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE,
- CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL,
-
- // Save
- CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
- CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
-
- // Audio
- CUTSCENE_ITEM_TYPE_AUDIO_PLAY,
- CUTSCENE_ITEM_TYPE_AUDIO_STOP,
- CUTSCENE_ITEM_TYPE_AUDIO_PAUSE,
- CUTSCENE_ITEM_TYPE_AUDIO_RESUME,
- CUTSCENE_ITEM_TYPE_AUDIO_FADE,
- CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT,
- CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN,
- CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP,
- CUTSCENE_ITEM_TYPE_AUDIO_SET,
-
- // Story
- CUTSCENE_ITEM_TYPE_STORY_SET_FLAG,
- CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG,
-
- // Party
- CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER,
- CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER,
- CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER,
- CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL,
-
- CUTSCENE_ITEM_TYPE_COUNT
-} cutsceneitemtype_t;
-
-/**
- * Shared no-op Load for item types with no JSON fields of their own to
- * parse (IDLE, RESTART, MODAL_CLOSE) - always succeeds without touching
- * item.
- *
- * @param itemObj The item's JSON object (unused).
- * @param item Destination item (unused).
- * @return Always errorOk().
- */
-errorret_t cutsceneItemLoadNoop(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.c b/src/dusk/rpg/cutscene/item/cutsceneprint.c
deleted file mode 100644
index 45b084cf..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneprint.c
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutsceneitem.h"
-#include "console/console.h"
-#include "json/cutscenejsonfield.h"
-
-void cutscenePrintStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- consolePrint("%s", item->print.text);
-}
-
-bool_t cutscenePrintUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutscenePrintLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonCopyString(
- itemObj, "text", NULL, item->print.text, CUTSCENE_PRINT_MAX_CHARS
- )) {
- errorThrow("Cutscene item (PRINT): missing/too-long 'text'");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.h b/src/dusk/rpg/cutscene/item/cutsceneprint.h
deleted file mode 100644
index 640dab3e..00000000
--- a/src/dusk/rpg/cutscene/item/cutsceneprint.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-#define CUTSCENE_PRINT_MAX_CHARS 128
-
-typedef struct {
- char_t text[CUTSCENE_PRINT_MAX_CHARS];
-} cutsceneprint_t;
-
-/**
- * Starts a print item (prints the item's text to the console).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutscenePrintStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a print item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutscenePrintUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a PRINT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutscenePrintLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt b/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt
deleted file mode 100644
index e6dfbd2d..00000000
--- a/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutsceneentityteleport.c
- cutsceneentitywalkto.c
- cutsceneentityremove.c
- cutsceneentityadd.c
- cutsceneentityturn.c
- cutsceneentitywalktoentity.c
-)
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c
deleted file mode 100644
index af8cca35..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "assert/assert.h"
-#include "rpg/cutscene/item/json/cutscenejsonentitytype.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneEntityAddStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uint8_t entIndex = entityGetAvailable();
- assertTrue(
- entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD"
- );
-
- entity_t *entity = &ENTITIES[entIndex];
- entityInit(entity, item->entityAdd.entityType);
- entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk.
-
- CUTSCENE_SYSTEM.refs.entityLastCreated = entity;
- CUTSCENE_SYSTEM.refs.entityLastRef = entity;
-}
-
-bool_t cutsceneEntityAddUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneEntityAddLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- entitytype_t entityTypeValue;
- if(!cutsceneJsonLookupEntityType(
- yyjson_get_str(yyjson_obj_get(itemObj, "entityType")), &entityTypeValue
- )) {
- errorThrow("Cutscene item (ENTITY_ADD): invalid 'entityType'");
- }
- item->entityAdd.entityType = entityTypeValue;
- if(!cutsceneJsonReadWorldPos(
- yyjson_obj_get(itemObj, "position"), &item->entityAdd.position
- )) {
- errorThrow("Cutscene item (ENTITY_ADD): 'position' must be [x, y, z]");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h
deleted file mode 100644
index 440cd415..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/entity/entitytype.h"
-#include "rpg/overworld/worldpos.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- entitytype_t entityType;
- worldpos_t position;
-} cutsceneentityadd_t;
-
-/**
- * Starts an entity add step (spawns the entity into the world immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityAddStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity add step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneEntityAddUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_ADD cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityAddLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c
deleted file mode 100644
index 3a1a02c2..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-
-void cutsceneEntityRemoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- entity_t *entity =
- cutsceneSystemGetEntity(item->entityRemove.entityIndex);
- entitySetChunk(entity, CHUNK_INDEX_INVALID);
- entity->type = ENTITY_TYPE_NULL;
-}
-
-bool_t cutsceneEntityRemoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneEntityRemoveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"), &item->entityRemove.entityIndex
- )) {
- errorThrow("Cutscene item (ENTITY_REMOVE): invalid 'entityIndex'");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h
deleted file mode 100644
index 26eb81d1..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t entityIndex;
-} cutsceneentityremove_t;
-
-/**
- * Starts an entity remove step (removes the entity from the world immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityRemoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity remove step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneEntityRemoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_REMOVE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityRemoveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c
deleted file mode 100644
index b63608fb..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneEntityTeleportStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- entityPositionSet(
- cutsceneSystemGetEntity(item->entityTeleport.entityIndex),
- item->entityTeleport.target
- );
-}
-
-bool_t cutsceneEntityTeleportUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneEntityTeleportLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"),
- &item->entityTeleport.entityIndex
- )) {
- errorThrow("Cutscene item (ENTITY_TELEPORT): invalid 'entityIndex'");
- }
- if(!cutsceneJsonReadWorldPos(
- yyjson_obj_get(itemObj, "target"), &item->entityTeleport.target
- )) {
- errorThrow(
- "Cutscene item (ENTITY_TELEPORT): 'target' must be [x, y, z]"
- );
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h
deleted file mode 100644
index 029771d9..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/worldpos.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t entityIndex;
- worldpos_t target;
-} cutsceneentityteleport_t;
-
-/**
- * Starts an entity teleport item (teleports the entity immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityTeleportStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity teleport item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneEntityTeleportUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_TELEPORT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityTeleportLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c
deleted file mode 100644
index 00b7f7cb..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-#include "rpg/cutscene/item/json/cutscenejsonentitydir.h"
-
-void cutsceneEntityTurnStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
-}
-
-bool_t cutsceneEntityTurnUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex);
- if(
- entity->direction == item->entityTurn.direction &&
- entity->animation == ENTITY_ANIM_IDLE
- ) return true;
-
- entityTurn(entity, item->entityTurn.direction);
- return false;
-}
-
-errorret_t cutsceneEntityTurnLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"), &item->entityTurn.entityIndex
- )) {
- errorThrow("Cutscene item (ENTITY_TURN): invalid 'entityIndex'");
- }
- entitydir_t dirValue;
- if(!cutsceneJsonLookupEntityDir(
- yyjson_get_str(yyjson_obj_get(itemObj, "direction")), &dirValue
- )) {
- errorThrow("Cutscene item (ENTITY_TURN): invalid 'direction'");
- }
- item->entityTurn.direction = dirValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h
deleted file mode 100644
index 164deaf7..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/entity/entitydir.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t entityIndex;
- entitydir_t direction;
-} cutsceneentityturn_t;
-
-/**
- * Starts an entity turn step. The turn itself is driven from Update, since
- * the entity may still be finishing a previous action.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityTurnStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity turn step, retrying entityTurn until it takes effect
- * and its animation completes.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the entity is idle and facing the target direction.
- */
-bool_t cutsceneEntityTurnUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_TURN cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityTurnLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c
deleted file mode 100644
index 16a82a08..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entitypathstep.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "assert/assert.h"
-
-void cutsceneEntityWalkToStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- assertTrue(
- item->entityWalkTo.count <= CUTSCENE_ENTITY_WALK_TO_MAX,
- "Too many waypoints in CUTSCENE_ENTITY_WALK_TO"
- );
- data->entityWalkTo.currentIndex = 0;
-}
-
-bool_t cutsceneEntityWalkToUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uint8_t i = data->entityWalkTo.currentIndex;
- entity_t *e = cutsceneSystemGetEntity(item->entityWalkTo.entityIndex);
- if(!entityPathStep(
- e,
- item->entityWalkTo.positions[i],
- item->entityWalkTo.walkAround
- )) return false;
- i++;
- if(i < item->entityWalkTo.count) {
- data->entityWalkTo.currentIndex = i;
- return false;
- }
- return true;
-}
-
-errorret_t cutsceneEntityWalkToLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"), &item->entityWalkTo.entityIndex
- )) {
- errorThrow("Cutscene item (ENTITY_WALK_TO): invalid 'entityIndex'");
- }
- item->entityWalkTo.walkAround =
- cutsceneJsonGetBool(itemObj, "walkAround", true);
-
- yyjson_val *positions = yyjson_obj_get(itemObj, "positions");
- size_t count = yyjson_arr_size(positions);
- if(
- !yyjson_is_arr(positions) || count == 0 ||
- count > CUTSCENE_ENTITY_WALK_TO_MAX
- ) {
- errorThrow(
- "Cutscene item (ENTITY_WALK_TO): 'positions' must have 1-%u entries "
- "of [x, y, z]",
- (uint32_t)CUTSCENE_ENTITY_WALK_TO_MAX
- );
- }
-
- yyjson_arr_iter iter = yyjson_arr_iter_with(positions);
- yyjson_val *posVal;
- size_t i = 0;
- while((posVal = yyjson_arr_iter_next(&iter)) != NULL) {
- if(!cutsceneJsonReadWorldPos(posVal, &item->entityWalkTo.positions[i])) {
- errorThrow(
- "Cutscene item (ENTITY_WALK_TO): position %u must be [x, y, z]",
- (uint32_t)i
- );
- }
- i++;
- }
- item->entityWalkTo.count = (uint8_t)count;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h
deleted file mode 100644
index 2ad31f09..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/worldpos.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-// Maximum number of waypoints a single ENTITY_WALK_TO/
-// CUTSCENE_ENTITY_WALK_PATH may hold - generous relative to every current
-// asset/C-authored cutscene (none use more than a couple waypoints).
-#define CUTSCENE_ENTITY_WALK_TO_MAX 16
-
-typedef struct {
- uint8_t entityIndex;
- worldpos_t positions[CUTSCENE_ENTITY_WALK_TO_MAX];
- uint8_t count;
- bool_t walkAround;
-} cutsceneentitywalkto_t;
-
-typedef struct {
- uint8_t currentIndex;
-} cutsceneentitywalktodata_t;
-
-/**
- * Starts an entity walk-to item (resets the waypoint index).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityWalkToStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity walk-to item (steps the entity toward the next waypoint).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once all waypoints have been reached.
- */
-bool_t cutsceneEntityWalkToUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_WALK_TO cutscene item's JSON fields into item, writing
- * its "positions" array directly into item->entityWalkTo.positions (at
- * most CUTSCENE_ENTITY_WALK_TO_MAX entries).
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityWalkToLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c
deleted file mode 100644
index 208beea7..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "rpg/entity/entitypathstep.h"
-#include "rpg/overworld/map.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-
-void cutsceneEntityWalkToEntityStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
-}
-
-bool_t cutsceneEntityWalkToEntityUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- entity_t *entity = cutsceneSystemGetEntity(
- item->entityWalkToEntity.entityIndex
- );
- entity_t *target = cutsceneSystemGetEntity(
- item->entityWalkToEntity.targetEntityIndex
- );
-
- worldpos_t dest = {
- .x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX),
- .y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY),
- .z = target->position.z
- };
-
- worldunit_t z;
- if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
-
- return entityPathStep(entity, dest, true);
-}
-
-errorret_t cutsceneEntityWalkToEntityLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"),
- &item->entityWalkToEntity.entityIndex
- ) || !cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "targetEntityIndex"),
- &item->entityWalkToEntity.targetEntityIndex
- )) {
- errorThrow(
- "Cutscene item (ENTITY_WALK_TO_ENTITY): invalid entity index"
- );
- }
- item->entityWalkToEntity.offsetX =
- (worldunit_t)yyjson_get_int(yyjson_obj_get(itemObj, "offsetX"));
- item->entityWalkToEntity.offsetY =
- (worldunit_t)yyjson_get_int(yyjson_obj_get(itemObj, "offsetY"));
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h
deleted file mode 100644
index 2dc0c254..00000000
--- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/worldpos.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t entityIndex;
- uint8_t targetEntityIndex;
- worldunit_t offsetX;
- worldunit_t offsetY;
-} cutsceneentitywalktoentity_t;
-
-/**
- * Starts an entity walk-to-entity item. No setup is needed, the destination
- * is recomputed from the target's live position every Update.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEntityWalkToEntityStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an entity walk-to-entity item. Re-reads the target entity's
- * current position each frame, applies the X/Y offset, resolves the
- * destination Z from nearby terrain (to account for ramps), and steps
- * the entity toward it.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the entity has reached the target's side.
- */
-bool_t cutsceneEntityWalkToEntityUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ENTITY_WALK_TO_ENTITY cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEntityWalkToEntityLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/item/CMakeLists.txt
deleted file mode 100644
index df5ebc21..00000000
--- a/src/dusk/rpg/cutscene/item/item/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutsceneitemgive.c
-)
diff --git a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c
deleted file mode 100644
index 975aada8..00000000
--- a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/item/itemgive.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-
-void cutsceneItemGiveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- itemGive(item->itemGive.item, item->itemGive.quantity);
-}
-
-bool_t cutsceneItemGiveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return !uiTextboxMainIsActive();
-}
-
-errorret_t cutsceneItemGiveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- item->itemGive.item =
- (itemid_t)yyjson_get_int(yyjson_obj_get(itemObj, "item"));
- item->itemGive.quantity =
- (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "quantity"));
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h
deleted file mode 100644
index 06762ba6..00000000
--- a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/item/item.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- itemid_t item;
- uint8_t quantity;
-} cutsceneitemgive_t;
-
-/**
- * Starts a give-item step (adds the item to the player's backpack immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneItemGiveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a give-item step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneItemGiveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an ITEM_GIVE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneItemGiveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt
deleted file mode 100644
index 60a8f286..00000000
--- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenejsonenum.c
- cutscenejsonfield.c
- cutscenejsonentityindex.c
- cutscenejsonareaid.c
- cutscenejsonpauseflags.c
- cutscenejsoneasing.c
- cutscenejsonemoji.c
- cutscenejsonentitytype.c
- cutscenejsonentitydir.c
- cutscenejsonbattlestate.c
- cutscenejsonscenetype.c
- cutscenejsonuiscreentype.c
- cutscenejsonaudiochannel.c
- cutscenejsonstoryflag.c
-)
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c
deleted file mode 100644
index 36d1db38..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonareaid.h"
-#include "util/string.h"
-
-bool_t cutsceneJsonGetAreaId(yyjson_val *val, uint8_t *outAreaId) {
- if(yyjson_is_str(val)) {
- const char_t *name = yyjson_get_str(val);
- if(stringCompareInsensitive(name, "LAST_CREATED") == 0) {
- *outAreaId = CUTSCENE_JSON_AREA_SENTINEL_LAST_CREATED;
- return true;
- }
- return false;
- }
- if(!yyjson_is_int(val)) return false;
- *outAreaId = (uint8_t)yyjson_get_int(val);
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h
deleted file mode 100644
index d12ee505..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "yyjson.h"
-
-// Sentinel accepted in place of a literal area id - see cutscenesystem.h's
-// CUTSCENE_REF_AREA_LAST_CREATED. Kept local since it's a JSON-authoring
-// convenience, not part of the item struct definitions themselves.
-#define CUTSCENE_JSON_AREA_SENTINEL_LAST_CREATED ((uint8_t)0xFF)
-
-/**
- * Resolves a JSON value that's either the "LAST_CREATED" map area sentinel
- * string or a plain integer area id.
- *
- * @param val JSON value to resolve (a string or a number).
- * @param outAreaId Set to the resolved area id on success.
- * @return true if val resolved to a valid area id, false otherwise.
- */
-bool_t cutsceneJsonGetAreaId(yyjson_val *val, uint8_t *outAreaId);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c
deleted file mode 100644
index 065a6245..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonaudiochannel.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_AUDIO_CHANNEL_TABLE[] = {
- { "BGM_0", AUDIO_MIXER_CHANNEL_BGM_0 },
- { "VOICE_0", AUDIO_MIXER_CHANNEL_VOICE_0 },
- { "VOICE_1", AUDIO_MIXER_CHANNEL_VOICE_1 },
- { "SFX_0", AUDIO_MIXER_CHANNEL_SFX_0 },
- { "SFX_1", AUDIO_MIXER_CHANNEL_SFX_1 },
- { "SFX_2", AUDIO_MIXER_CHANNEL_SFX_2 },
- { "SFX_3", AUDIO_MIXER_CHANNEL_SFX_3 },
-};
-#define CUTSCENE_JSON_AUDIO_CHANNEL_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_AUDIO_CHANNEL_TABLE) / \
- sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupAudioChannel(
- const char_t *name,
- audiomixerchannel_t *out
-) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_AUDIO_CHANNEL_TABLE,
- CUTSCENE_JSON_AUDIO_CHANNEL_TABLE_COUNT, name, &value
- )) {
- return false;
- }
- *out = (audiomixerchannel_t)value;
- return true;
-}
-
-errorret_t cutsceneJsonLoadAudioChannelOnly(
- yyjson_val *itemObj,
- audiomixerchannel_t *outChannel
-) {
- if(!cutsceneJsonLookupAudioChannel(
- yyjson_get_str(yyjson_obj_get(itemObj, "channel")), outChannel
- )) {
- errorThrow("Cutscene item: invalid 'channel'");
- }
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h
deleted file mode 100644
index c6e2234f..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "error/error.h"
-#include "yyjson.h"
-#include "audio/mixer/audiomixerchannel.h"
-
-/**
- * Looks up name (case-insensitively) against the audio mixer channel
- * table.
- *
- * @param name Name to search for.
- * @param out Set to the matching channel on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupAudioChannel(
- const char_t *name,
- audiomixerchannel_t *out
-);
-
-/**
- * Reads itemObj's "channel" field via cutsceneJsonLookupAudioChannel,
- * erroring if missing/invalid - shared by every channel-only item type
- * (AUDIO_STOP/PAUSE/RESUME/FADE_WAIT), which differ only in which item
- * union field they store the result in.
- *
- * @param itemObj The item's JSON object.
- * @param outChannel Set to the parsed channel on success.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneJsonLoadAudioChannelOnly(
- yyjson_val *itemObj,
- audiomixerchannel_t *outChannel
-);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c
deleted file mode 100644
index 60993646..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonbattlestate.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = {
- { "OPENING", BATTLE_STATE_OPENING },
- { "PRE_ROUND", BATTLE_STATE_PRE_ROUND },
- { "SELECTION", BATTLE_STATE_SELECTION },
- { "EXECUTING", BATTLE_STATE_EXECUTING },
- { "POST_ROUND", BATTLE_STATE_POST_ROUND },
- { "LOST", BATTLE_STATE_LOST },
- { "WON", BATTLE_STATE_WON },
- { "FLED", BATTLE_STATE_FLED },
- { "ENDING", BATTLE_STATE_ENDING },
-};
-#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupBattleState(
- const char_t *name,
- battlestate_t *out
-) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_BATTLE_STATE_TABLE,
- CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value
- )) {
- return false;
- }
- *out = (battlestate_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h
deleted file mode 100644
index 1cf302f2..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/battle/state/battlestate.h"
-
-/**
- * Looks up name (case-insensitive) against every battlestate_t name.
- *
- * @param name Name to search for.
- * @param out Set to the matching state on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupBattleState(
- const char_t *name,
- battlestate_t *out
-);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c b/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c
deleted file mode 100644
index b70df9be..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsoneasing.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_EASING_TABLE[] = {
- { "LINEAR", EASING_LINEAR },
- { "IN_SINE", EASING_IN_SINE },
- { "OUT_SINE", EASING_OUT_SINE },
- { "IN_OUT_SINE", EASING_IN_OUT_SINE },
- { "IN_QUAD", EASING_IN_QUAD },
- { "OUT_QUAD", EASING_OUT_QUAD },
- { "IN_OUT_QUAD", EASING_IN_OUT_QUAD },
- { "IN_CUBIC", EASING_IN_CUBIC },
- { "OUT_CUBIC", EASING_OUT_CUBIC },
- { "IN_OUT_CUBIC", EASING_IN_OUT_CUBIC },
- { "IN_QUART", EASING_IN_QUART },
- { "OUT_QUART", EASING_OUT_QUART },
- { "IN_OUT_QUART", EASING_IN_OUT_QUART },
- { "IN_BACK", EASING_IN_BACK },
- { "OUT_BACK", EASING_OUT_BACK },
- { "IN_OUT_BACK", EASING_IN_OUT_BACK },
-};
-#define CUTSCENE_JSON_EASING_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_EASING_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupEasing(const char_t *name, easingtype_t *out) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_EASING_TABLE, CUTSCENE_JSON_EASING_TABLE_COUNT, name,
- &value
- )) {
- return false;
- }
- *out = (easingtype_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h b/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h
deleted file mode 100644
index 26d282e5..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "animation/easing.h"
-
-/**
- * Looks up name (case-insensitively) against the easing type table.
- *
- * @param name Name to search for.
- * @param out Set to the matching easing type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEasing(const char_t *name, easingtype_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c
deleted file mode 100644
index ce878f41..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonemoji.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_UI_EMOJI_TABLE[] = {
- { "NULL", UI_EMOJI_NULL },
- { "QUESTION_MARK", UI_EMOJI_QUESTION_MARK },
- { "EXCLAMATION_MARK", UI_EMOJI_EXCLAMATION_MARK },
-};
-#define CUTSCENE_JSON_UI_EMOJI_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_UI_EMOJI_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupEmoji(const char_t *name, uiemojitype_t *out) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_UI_EMOJI_TABLE, CUTSCENE_JSON_UI_EMOJI_TABLE_COUNT, name,
- &value
- )) {
- return false;
- }
- *out = (uiemojitype_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h
deleted file mode 100644
index 55b483c3..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "ui/rpg/uiemoji.h"
-
-/**
- * Looks up name (case-insensitively) against the emoji type table.
- *
- * @param name Name to search for.
- * @param out Set to the matching emoji type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEmoji(const char_t *name, uiemojitype_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c
deleted file mode 100644
index 85ac7baa..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonentitydir.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_ENTITY_DIR_TABLE[] = {
- { "NORTH", ENTITY_DIR_NORTH },
- { "EAST", ENTITY_DIR_EAST },
- { "SOUTH", ENTITY_DIR_SOUTH },
- { "WEST", ENTITY_DIR_WEST },
- { "UP", ENTITY_DIR_UP },
- { "RIGHT", ENTITY_DIR_RIGHT },
- { "DOWN", ENTITY_DIR_DOWN },
- { "LEFT", ENTITY_DIR_LEFT },
-};
-#define CUTSCENE_JSON_ENTITY_DIR_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_ENTITY_DIR_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupEntityDir(const char_t *name, entitydir_t *out) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_ENTITY_DIR_TABLE, CUTSCENE_JSON_ENTITY_DIR_TABLE_COUNT,
- name, &value
- )) {
- return false;
- }
- *out = (entitydir_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h
deleted file mode 100644
index b3a3d226..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/entity/entitydir.h"
-
-/**
- * Looks up name (case-insensitively) against the entity direction table.
- *
- * @param name Name to search for.
- * @param out Set to the matching direction on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEntityDir(const char_t *name, entitydir_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c
deleted file mode 100644
index 9cc076bf..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonentityindex.h"
-#include "util/string.h"
-
-bool_t cutsceneJsonGetEntityIndex(yyjson_val *val, uint8_t *outIndex) {
- if(yyjson_is_str(val)) {
- const char_t *name = yyjson_get_str(val);
- if(stringCompareInsensitive(name, "INTERACT") == 0) {
- *outIndex = CUTSCENE_JSON_ENTITY_SENTINEL_INTERACT;
- return true;
- }
- if(stringCompareInsensitive(name, "INTERACTED") == 0) {
- *outIndex = CUTSCENE_JSON_ENTITY_SENTINEL_INTERACTED;
- return true;
- }
- return false;
- }
- if(!yyjson_is_int(val)) return false;
- *outIndex = (uint8_t)yyjson_get_int(val);
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h
deleted file mode 100644
index e7740d63..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "yyjson.h"
-
-// Sentinels accepted in place of a literal entity index - see
-// cutscenesystem.h's CUTSCENE_ENTITY_INTERACT/INTERACTED. Kept local
-// since they're a JSON-authoring convenience, not part of the item
-// struct definitions themselves.
-#define CUTSCENE_JSON_ENTITY_SENTINEL_INTERACT ((uint8_t)0xFE)
-#define CUTSCENE_JSON_ENTITY_SENTINEL_INTERACTED ((uint8_t)0xFD)
-
-/**
- * Resolves a JSON value that's either an entity index sentinel string
- * ("INTERACT"/"INTERACTED") or a plain integer entity index.
- *
- * @param val JSON value to resolve (a string or a number).
- * @param outIndex Set to the resolved index on success.
- * @return true if val resolved to a valid index, false otherwise.
- */
-bool_t cutsceneJsonGetEntityIndex(yyjson_val *val, uint8_t *outIndex);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c
deleted file mode 100644
index ac2f9e13..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonentitytype.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_ENTITY_TYPE_TABLE[] = {
- { "NULL", ENTITY_TYPE_NULL },
- { "PLAYER", ENTITY_TYPE_PLAYER },
- { "NPC", ENTITY_TYPE_NPC },
- { "ITEM", ENTITY_TYPE_ITEM },
-};
-#define CUTSCENE_JSON_ENTITY_TYPE_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_ENTITY_TYPE_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupEntityType(const char_t *name, entitytype_t *out) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_ENTITY_TYPE_TABLE, CUTSCENE_JSON_ENTITY_TYPE_TABLE_COUNT,
- name, &value
- )) {
- return false;
- }
- *out = (entitytype_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h
deleted file mode 100644
index f08b45e8..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/entity/entitytype.h"
-
-/**
- * Looks up name (case-insensitively) against the entity type table.
- *
- * @param name Name to search for.
- * @param out Set to the matching entity type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEntityType(const char_t *name, entitytype_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c
deleted file mode 100644
index 3c7a1d67..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonenum.h"
-#include "util/string.h"
-
-bool_t cutsceneJsonLookupEnum(
- const cutscenejsonenumentry_t *table,
- const size_t tableCount,
- const char_t *name,
- int32_t *outValue
-) {
- if(name == NULL) return false;
- for(size_t i = 0; i < tableCount; i++) {
- if(stringEquals(table[i].name, name)) {
- *outValue = table[i].value;
- return true;
- }
- }
- return false;
-}
-
-bool_t cutsceneJsonLookupEnumInsensitive(
- const cutscenejsonenumentry_t *table,
- const size_t tableCount,
- const char_t *name,
- int32_t *outValue
-) {
- if(name == NULL) return false;
- for(size_t i = 0; i < tableCount; i++) {
- if(stringCompareInsensitive(table[i].name, name) == 0) {
- *outValue = table[i].value;
- return true;
- }
- }
- return false;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h
deleted file mode 100644
index 8130007d..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct {
- const char_t *name;
- int32_t value;
-} cutscenejsonenumentry_t;
-
-/**
- * Looks up name in a {name, value} table via stringEquals - exact case,
- * used only for the top-level item "type" name (the one enum-ish field
- * the original tool's ITEM_TYPE dict looked up without .upper()).
- *
- * @param table Table to search.
- * @param tableCount Number of entries in table.
- * @param name Name to search for.
- * @param outValue Set to the matching entry's value on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEnum(
- const cutscenejsonenumentry_t *table,
- const size_t tableCount,
- const char_t *name,
- int32_t *outValue
-);
-
-/**
- * Looks up name in a {name, value} table via stringCompareInsensitive -
- * every other enum-ish field (pause flags, entity/scene/battle/audio
- * enums, ...) is authored case-insensitively, matching the original
- * tool's ENUM_TABLE[name.upper()] lookups.
- *
- * @param table Table to search.
- * @param tableCount Number of entries in table.
- * @param name Name to search for.
- * @param outValue Set to the matching entry's value on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupEnumInsensitive(
- const cutscenejsonenumentry_t *table,
- const size_t tableCount,
- const char_t *name,
- int32_t *outValue
-);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c
deleted file mode 100644
index 25b78d86..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonfield.h"
-#include "util/string.h"
-
-bool_t cutsceneJsonCopyString(
- yyjson_val *obj,
- const char_t *key,
- const char_t *defaultValue,
- char_t *dest,
- const size_t destCapacity
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- const char_t *str = yyjson_get_str(val);
- size_t len;
- if(str == NULL) {
- if(defaultValue == NULL) return false;
- str = defaultValue;
- len = (size_t)stringFormat(NULL, 0, "%s", str);
- } else {
- len = yyjson_get_len(val);
- }
- if(len >= destCapacity) return false;
- stringCopy(dest, str, destCapacity);
- return true;
-}
-
-bool_t cutsceneJsonReadWorldPos(yyjson_val *arr, worldpos_t *out) {
- if(!yyjson_is_arr(arr) || yyjson_arr_size(arr) != 3) return false;
- yyjson_val *x = yyjson_arr_get(arr, 0);
- yyjson_val *y = yyjson_arr_get(arr, 1);
- yyjson_val *z = yyjson_arr_get(arr, 2);
- if(!yyjson_is_int(x) || !yyjson_is_int(y) || !yyjson_is_int(z)) return false;
- out->x = (worldunit_t)yyjson_get_int(x);
- out->y = (worldunit_t)yyjson_get_int(y);
- out->z = (worldunit_t)yyjson_get_int(z);
- return true;
-}
-
-int32_t cutsceneJsonGetInt(
- yyjson_val *obj,
- const char_t *key,
- const int32_t defaultValue
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- return val == NULL ? defaultValue : (int32_t)yyjson_get_int(val);
-}
-
-float_t cutsceneJsonGetFloat(
- yyjson_val *obj,
- const char_t *key,
- const float_t defaultValue
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- return val == NULL ? defaultValue : (float_t)yyjson_get_num(val);
-}
-
-bool_t cutsceneJsonGetBool(
- yyjson_val *obj,
- const char_t *key,
- const bool_t defaultValue
-) {
- yyjson_val *val = yyjson_obj_get(obj, key);
- return val == NULL ? defaultValue : yyjson_get_bool(val);
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h
deleted file mode 100644
index a73f07fe..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/overworld/worldpos.h"
-#include "yyjson.h"
-
-/**
- * Reads obj[key] as a string (falling back to defaultValue if the key is
- * absent and defaultValue is non-NULL) into dest, a fixed-capacity
- * char_t[destCapacity] item field. Fails rather than truncating/asserting
- * if the string doesn't fit - this is untrusted, authored file content,
- * not an in-memory invariant.
- *
- * @param obj The item's JSON object.
- * @param key Field name to read.
- * @param defaultValue Value to use if key is absent, or NULL to require it.
- * @param dest Destination buffer.
- * @param destCapacity Capacity of dest, including the null terminator.
- * @return true on success, false if the field is missing (with no
- * default) or exceeds destCapacity.
- */
-bool_t cutsceneJsonCopyString(
- yyjson_val *obj,
- const char_t *key,
- const char_t *defaultValue,
- char_t *dest,
- const size_t destCapacity
-);
-
-/**
- * Reads a 3-element JSON array of integers into a worldpos_t.
- *
- * @param arr JSON array value, expected to hold exactly 3 numbers.
- * @param out Set to the decoded position on success.
- * @return true if arr was a valid 3-element numeric array, false otherwise.
- */
-bool_t cutsceneJsonReadWorldPos(yyjson_val *arr, worldpos_t *out);
-
-/**
- * Reads obj[key] as an integer, falling back to defaultValue if the key
- * is absent.
- *
- * @param obj The item's JSON object.
- * @param key Field name to read.
- * @param defaultValue Value to use if key is absent.
- * @return The field's value, or defaultValue if absent.
- */
-int32_t cutsceneJsonGetInt(
- yyjson_val *obj,
- const char_t *key,
- const int32_t defaultValue
-);
-
-/**
- * Reads obj[key] as a float, falling back to defaultValue if the key is
- * absent.
- *
- * @param obj The item's JSON object.
- * @param key Field name to read.
- * @param defaultValue Value to use if key is absent.
- * @return The field's value, or defaultValue if absent.
- */
-float_t cutsceneJsonGetFloat(
- yyjson_val *obj,
- const char_t *key,
- const float_t defaultValue
-);
-
-/**
- * Reads obj[key] as a boolean, falling back to defaultValue if the key is
- * absent.
- *
- * @param obj The item's JSON object.
- * @param key Field name to read.
- * @param defaultValue Value to use if key is absent.
- * @return The field's value, or defaultValue if absent.
- */
-bool_t cutsceneJsonGetBool(
- yyjson_val *obj,
- const char_t *key,
- const bool_t defaultValue
-);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c
deleted file mode 100644
index 29eb1b54..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonpauseflags.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_PAUSE_FLAG_TABLE[] = {
- { "NPC", CUTSCENE_PAUSE_NPC },
- { "PLAYER", CUTSCENE_PAUSE_PLAYER },
- { "WORLD", CUTSCENE_PAUSE_WORLD },
- { "BATTLE", CUTSCENE_PAUSE_BATTLE },
-};
-#define CUTSCENE_JSON_PAUSE_FLAG_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_PAUSE_FLAG_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonParsePauseFlags(yyjson_val *arr, cutscenepause_t *out) {
- if(!yyjson_is_arr(arr)) return false;
-
- cutscenepause_t value = CUTSCENE_PAUSE_NONE;
- yyjson_arr_iter iter = yyjson_arr_iter_with(arr);
- yyjson_val *flagVal;
- while((flagVal = yyjson_arr_iter_next(&iter)) != NULL) {
- int32_t flagValue;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_PAUSE_FLAG_TABLE, CUTSCENE_JSON_PAUSE_FLAG_TABLE_COUNT,
- yyjson_get_str(flagVal), &flagValue
- )) {
- return false;
- }
- value |= (cutscenepause_t)flagValue;
- }
- *out = value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h
deleted file mode 100644
index b6bccb6b..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/cutscene/cutscenepause.h"
-#include "yyjson.h"
-
-/**
- * Parses a JSON array of pause flag name strings, OR-ing each into out.
- * Shared by the top-level "pause" field (cutsceneParseDoc) and the
- * SET_PAUSE item's "flags" field - both authored as the same shape.
- *
- * @param arr JSON array of pause flag name strings.
- * @param out Set to the OR'd-together flags on success (CUTSCENE_PAUSE_NONE
- * if arr is empty).
- * @return true on success, false if arr is not an array or contains an
- * invalid flag name.
- */
-bool_t cutsceneJsonParsePauseFlags(yyjson_val *arr, cutscenepause_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c
deleted file mode 100644
index 70a5616f..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonscenetype.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_SCENE_TYPE_TABLE[] = {
- { "NULL", SCENE_TYPE_NULL },
- { "INITIAL", SCENE_TYPE_INITIAL },
- { "OVERWORLD", SCENE_TYPE_OVERWORLD },
- { "BATTLE", SCENE_TYPE_BATTLE },
-};
-#define CUTSCENE_JSON_SCENE_TYPE_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_SCENE_TYPE_TABLE) / sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupSceneType(const char_t *name, scenetype_t *out) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_SCENE_TYPE_TABLE, CUTSCENE_JSON_SCENE_TYPE_TABLE_COUNT,
- name, &value
- )) {
- return false;
- }
- *out = (scenetype_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h
deleted file mode 100644
index 58a38271..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "scene/scenetype.h"
-
-/**
- * Looks up name (case-insensitively) against the scene type table.
- *
- * @param name Name to search for.
- * @param out Set to the matching scene type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupSceneType(const char_t *name, scenetype_t *out);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c
deleted file mode 100644
index 2eb14384..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonstoryflag.h"
-
-bool_t cutsceneJsonGetStoryFlag(yyjson_val *val, storyflag_t *outFlag) {
- if(!yyjson_is_str(val)) return false;
-
- storyflag_t flag = storyGetFlagByName(yyjson_get_str(val));
- if(flag >= STORY_FLAG_COUNT) return false;
-
- *outFlag = flag;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h
deleted file mode 100644
index d5152fde..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "yyjson.h"
-#include "rpg/story/story.h"
-
-/**
- * Resolves a JSON string value naming a story flag (via storyGetFlagByName)
- * into a storyflag_t.
- *
- * @param val JSON value to resolve (a string).
- * @param outFlag Set to the resolved story flag id on success.
- * @return true if val resolved to a known story flag, false otherwise.
- */
-bool_t cutsceneJsonGetStoryFlag(yyjson_val *val, storyflag_t *outFlag);
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c
deleted file mode 100644
index 4d53a349..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "cutscenejsonuiscreentype.h"
-#include "cutscenejsonenum.h"
-
-static const cutscenejsonenumentry_t CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE[] = {
- { "MAIN_MENU", UI_SCREEN_TYPE_MAIN_MENU },
-};
-#define CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE_COUNT \
- (sizeof(CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE) / \
- sizeof(cutscenejsonenumentry_t))
-
-bool_t cutsceneJsonLookupUiScreenType(
- const char_t *name,
- uiscreentype_t *out
-) {
- int32_t value;
- if(!cutsceneJsonLookupEnumInsensitive(
- CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE,
- CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE_COUNT, name, &value
- )) {
- return false;
- }
- *out = (uiscreentype_t)value;
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h
deleted file mode 100644
index fd1f984d..00000000
--- a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/cutscene/item/ui/cutsceneuishow.h"
-
-/**
- * Looks up name (case-insensitively) against the UI screen type table.
- *
- * @param name Name to search for.
- * @param out Set to the matching screen type on success.
- * @return true if a matching entry was found, false otherwise.
- */
-bool_t cutsceneJsonLookupUiScreenType(
- const char_t *name,
- uiscreentype_t *out
-);
diff --git a/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt b/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt
deleted file mode 100644
index cd32d7f6..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenemapareaadd.c
- cutscenemaparearemove.c
- cutscenemapareawait.c
-)
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c
deleted file mode 100644
index 8b408a0b..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void cutsceneMapAreaAddStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- CUTSCENE_SYSTEM.refs.areaLastCreated = mapAreaAdd(
- item->mapAreaAdd.min,
- item->mapAreaAdd.max,
- item->mapAreaAdd.callback,
- item->mapAreaAdd.notify,
- item->mapAreaAdd.trigger
- );
-}
-
-bool_t cutsceneMapAreaAddUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h
deleted file mode 100644
index b4135a65..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/overworld/maparea.h"
-#include "rpg/cutscene/item/maparea/cutscenemapareawait.h"
-#include "rpg/cutscene/item/maparea/cutscenemaparearemove.h"
-
-typedef struct {
- worldpos_t min;
- worldpos_t max;
- mapareacallback_t callback;
- uint8_t notify;
- uint8_t trigger;
-} cutscenemapareaadd_t;
-
-/**
- * Starts a map area add step (adds the area immediately, storing its ID
- * in CUTSCENE_SYSTEM.refs.areaLastCreated).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneMapAreaAddStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a map area add step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneMapAreaAddUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c
deleted file mode 100644
index 47a0065d..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/overworld/maparea.h"
-#include "rpg/cutscene/item/json/cutscenejsonareaid.h"
-
-void cutsceneMapAreaRemoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- mapAreaRemove(cutsceneSystemGetAreaId(item->mapAreaRemove.areaId));
-}
-
-bool_t cutsceneMapAreaRemoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneMapAreaRemoveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetAreaId(
- yyjson_obj_get(itemObj, "areaId"), &item->mapAreaRemove.areaId
- )) {
- errorThrow("Cutscene item (MAP_AREA_REMOVE): invalid 'areaId'");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h
deleted file mode 100644
index c85ba9be..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t areaId;
-} cutscenemaparearemove_t;
-
-/**
- * Starts a map area remove step (removes the area immediately). Accepts
- * CUTSCENE_REF_AREA_LAST_CREATED in place of a literal area ID.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneMapAreaRemoveStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a map area remove step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneMapAreaRemoveUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a MAP_AREA_REMOVE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneMapAreaRemoveLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c
deleted file mode 100644
index 1acbb963..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/overworld/maparea.h"
-#include "assert/assert.h"
-#include "rpg/cutscene/item/json/cutscenejsonareaid.h"
-
-void cutsceneMapAreaWaitStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- assertTrue(
- item->mapAreaWait.count <= CUTSCENE_MAP_AREA_WAIT_MAX,
- "Too many areas in CUTSCENE_MAP_AREA_WAIT"
- );
-
- for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
- uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
- data->mapAreaWait.baseline[i] = MAP_AREAS[areaId].triggerCount;
- }
-}
-
-bool_t cutsceneMapAreaWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
- uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
- if(MAP_AREAS[areaId].triggerCount != data->mapAreaWait.baseline[i]) {
- return true;
- }
- }
-
- return false;
-}
-
-errorret_t cutsceneMapAreaWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *areaIds = yyjson_obj_get(itemObj, "areaIds");
- size_t count = yyjson_arr_size(areaIds);
- if(
- !yyjson_is_arr(areaIds) || count == 0 ||
- count > CUTSCENE_MAP_AREA_WAIT_MAX
- ) {
- errorThrow(
- "Cutscene item (MAP_AREA_WAIT): 'areaIds' must have 1-%u entries",
- (uint32_t)CUTSCENE_MAP_AREA_WAIT_MAX
- );
- }
- yyjson_arr_iter iter = yyjson_arr_iter_with(areaIds);
- yyjson_val *idVal;
- size_t i = 0;
- while((idVal = yyjson_arr_iter_next(&iter)) != NULL) {
- if(!cutsceneJsonGetAreaId(idVal, &item->mapAreaWait.areaIds[i])) {
- errorThrow(
- "Cutscene item (MAP_AREA_WAIT): areaIds[%u] is invalid", (uint32_t)i
- );
- }
- i++;
- }
- item->mapAreaWait.count = (uint8_t)count;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h
deleted file mode 100644
index 0787170f..00000000
--- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/** Maximum number of areas a single CUTSCENE_MAP_AREA_WAIT may watch. */
-#define CUTSCENE_MAP_AREA_WAIT_MAX 4
-
-typedef struct {
- uint8_t areaIds[CUTSCENE_MAP_AREA_WAIT_MAX];
- uint8_t count;
-} cutscenemapareawait_t;
-
-typedef struct {
- uint32_t baseline[CUTSCENE_MAP_AREA_WAIT_MAX];
-} cutscenemapareawaitdata_t;
-
-/**
- * Starts a map area wait step, snapshotting each watched area's current
- * trigger count.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneMapAreaWaitStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a map area wait step, completing once any watched area's
- * trigger count has changed since Start (i.e. its callback fired).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once any watched area has been triggered.
- */
-bool_t cutsceneMapAreaWaitUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a MAP_AREA_WAIT cutscene item's JSON fields into item, writing its
- * "areaIds" array directly into item->mapAreaWait.areaIds.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneMapAreaWaitLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/party/CMakeLists.txt b/src/dusk/rpg/cutscene/item/party/CMakeLists.txt
deleted file mode 100644
index 6f383c3e..00000000
--- a/src/dusk/rpg/cutscene/item/party/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenepartyaddmember.c
- cutscenepartyremovemember.c
- cutscenepartyhealmember.c
- cutscenepartyhealall.c
-)
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c
deleted file mode 100644
index 9612132e..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/party.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutscenePartyAddMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- partyAddMember(item->partyAddMember.memberIndex);
-}
-
-bool_t cutscenePartyAddMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutscenePartyAddMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- char_t key[PARTY_KEY_LENGTH_MAX];
- if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) {
- errorThrow("Cutscene item (PARTY_ADD_MEMBER): missing/invalid 'key'");
- }
-
- const uint8_t memberIndex = partyGetMemberIndexByKey(key);
- if(memberIndex == PARTY_MEMBER_NOT_FOUND) {
- errorThrow("Cutscene item (PARTY_ADD_MEMBER): no member with key '%s'", key);
- }
- item->partyAddMember.memberIndex = memberIndex;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h
deleted file mode 100644
index 28768ef1..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- // Resolved from the JSON 'key' field at load time (see
- // partyGetMemberIndexByKey) - not a raw JSON field itself.
- uint8_t memberIndex;
-} cutscenepartyaddmember_t;
-
-/**
- * Starts a PARTY_ADD_MEMBER item, activating the configured existing
- * roster member into the current save slot's active party order (see
- * partyAddMember). No-op if already active or there's no free order slot.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutscenePartyAddMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a PARTY_ADD_MEMBER item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutscenePartyAddMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a PARTY_ADD_MEMBER cutscene item's JSON fields into item:
- * 'key', a required string matching a party.jsonc member key, resolved to
- * its roster index via partyGetMemberIndexByKey.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutscenePartyAddMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c
deleted file mode 100644
index 3b3aa111..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/party.h"
-
-void cutscenePartyHealAllStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- partyHealAll();
-}
-
-bool_t cutscenePartyHealAllUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h
deleted file mode 100644
index e2d12858..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-/**
- * Starts a PARTY_HEAL_ALL item, fully restoring every occupied slot in
- * the current save slot's party roster (see partyHealAll).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutscenePartyHealAllStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a PARTY_HEAL_ALL item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutscenePartyHealAllUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c
deleted file mode 100644
index a4091e4e..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/party.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutscenePartyHealMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- partyHealMember(item->partyHealMember.memberIndex);
-}
-
-bool_t cutscenePartyHealMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutscenePartyHealMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- char_t key[PARTY_KEY_LENGTH_MAX];
- if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) {
- errorThrow("Cutscene item (PARTY_HEAL_MEMBER): missing/invalid 'key'");
- }
-
- const uint8_t memberIndex = partyGetMemberIndexByKey(key);
- if(memberIndex == PARTY_MEMBER_NOT_FOUND) {
- errorThrow("Cutscene item (PARTY_HEAL_MEMBER): no member with key '%s'", key);
- }
- item->partyHealMember.memberIndex = memberIndex;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h
deleted file mode 100644
index 3a36a86c..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- // Resolved from the JSON 'key' field at load time (see
- // partyGetMemberIndexByKey) - not a raw JSON field itself.
- uint8_t memberIndex;
-} cutscenepartyhealmember_t;
-
-/**
- * Starts a PARTY_HEAL_MEMBER item, fully restoring the configured member's
- * health/mp and reviving it if dead (see partyHealMember).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutscenePartyHealMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a PARTY_HEAL_MEMBER item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutscenePartyHealMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a PARTY_HEAL_MEMBER cutscene item's JSON fields into item:
- * 'key', a required string matching a party.jsonc member key, resolved to
- * its roster index via partyGetMemberIndexByKey.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutscenePartyHealMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c
deleted file mode 100644
index e636d75e..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/battle/party.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutscenePartyRemoveMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- partyRemoveMember(item->partyRemoveMember.memberIndex);
-}
-
-bool_t cutscenePartyRemoveMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutscenePartyRemoveMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- char_t key[PARTY_KEY_LENGTH_MAX];
- if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) {
- errorThrow("Cutscene item (PARTY_REMOVE_MEMBER): missing/invalid 'key'");
- }
-
- const uint8_t memberIndex = partyGetMemberIndexByKey(key);
- if(memberIndex == PARTY_MEMBER_NOT_FOUND) {
- errorThrow(
- "Cutscene item (PARTY_REMOVE_MEMBER): no member with key '%s'", key
- );
- }
- item->partyRemoveMember.memberIndex = memberIndex;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h
deleted file mode 100644
index 91809aea..00000000
--- a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- // Resolved from the JSON 'key' field at load time (see
- // partyGetMemberIndexByKey) - not a raw JSON field itself.
- uint8_t memberIndex;
-} cutscenepartyremovemember_t;
-
-/**
- * Starts a PARTY_REMOVE_MEMBER item, deactivating the configured member
- * from the current save slot's active party order (see partyRemoveMember)
- * - the roster member itself still exists, just no longer active.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutscenePartyRemoveMemberStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a PARTY_REMOVE_MEMBER item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutscenePartyRemoveMemberUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a PARTY_REMOVE_MEMBER cutscene item's JSON fields into item:
- * 'key', a required string matching a party.jsonc member key, resolved to
- * its roster index via partyGetMemberIndexByKey.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutscenePartyRemoveMemberLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/save/CMakeLists.txt b/src/dusk/rpg/cutscene/item/save/CMakeLists.txt
deleted file mode 100644
index 7ca216ee..00000000
--- a/src/dusk/rpg/cutscene/item/save/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenesavedevicecheck.c
- cutscenesaveloadallslots.c
-)
diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c b/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c
deleted file mode 100644
index 76d3a3fa..00000000
--- a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "save/save.h"
-
-void cutsceneSaveDeviceCheckCallback(savedevice_t *device, void *user) {
- const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
- cutsceneSystemGoTo(
- device != NULL ?
- item->saveDeviceCheck.successMarker :
- item->saveDeviceCheck.failureMarker
- );
-}
-
-void cutsceneSaveDeviceCheckStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- saveFindAvailableDevice(cutsceneSaveDeviceCheckCallback, NULL);
-}
-
-bool_t cutsceneSaveDeviceCheckUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
-
-errorret_t cutsceneSaveDeviceCheckLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(
- !cutsceneJsonCopyString(
- itemObj, "successMarker", NULL, item->saveDeviceCheck.successMarker,
- CUTSCENE_MARKER_NAME_MAX
- ) ||
- !cutsceneJsonCopyString(
- itemObj, "failureMarker", NULL, item->saveDeviceCheck.failureMarker,
- CUTSCENE_MARKER_NAME_MAX
- )
- ) {
- errorThrow(
- "Cutscene item (SAVE_DEVICE_CHECK): needs 'successMarker'/"
- "'failureMarker'"
- );
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h b/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h
deleted file mode 100644
index 3998dd4c..00000000
--- a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/cutscene/item/control/cutscenemarker.h"
-
-typedef struct {
- char_t successMarker[CUTSCENE_MARKER_NAME_MAX];
- char_t failureMarker[CUTSCENE_MARKER_NAME_MAX];
-} cutscenesavedevicecheck_t;
-
-/**
- * Starts a save-device-check item: (re)requests an available save
- * device via saveFindAvailableDevice. Never completes on its own - see
- * cutsceneSaveDeviceCheckUpdate.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneSaveDeviceCheckStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a save-device-check item. Like a CUTSCENE_MODAL_OPTIONS item,
- * this never completes on its own - once saveFindAvailableDevice's
- * callback fires, it jumps straight to the item's successMarker or
- * failureMarker via cutsceneSystemGoTo.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneSaveDeviceCheckUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a SAVE_DEVICE_CHECK cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneSaveDeviceCheckLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c b/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c
deleted file mode 100644
index 1b368723..00000000
--- a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-#include "save/save.h"
-
-void cutsceneSaveLoadAllSlotsStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- errorret_t result = saveLoadAllSlots();
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- cutsceneSystemGoTo(item->saveLoadAllSlots.failureMarker);
- return;
- }
-
- cutsceneSystemGoTo(item->saveLoadAllSlots.successMarker);
-}
-
-bool_t cutsceneSaveLoadAllSlotsUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
-
-errorret_t cutsceneSaveLoadAllSlotsLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(
- !cutsceneJsonCopyString(
- itemObj, "successMarker", NULL, item->saveLoadAllSlots.successMarker,
- CUTSCENE_MARKER_NAME_MAX
- ) ||
- !cutsceneJsonCopyString(
- itemObj, "failureMarker", NULL, item->saveLoadAllSlots.failureMarker,
- CUTSCENE_MARKER_NAME_MAX
- )
- ) {
- errorThrow(
- "Cutscene item (SAVE_LOAD_ALL_SLOTS): needs 'successMarker'/"
- "'failureMarker'"
- );
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h b/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h
deleted file mode 100644
index 18b32b9d..00000000
--- a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/cutscene/item/control/cutscenemarker.h"
-
-typedef struct {
- char_t successMarker[CUTSCENE_MARKER_NAME_MAX];
- char_t failureMarker[CUTSCENE_MARKER_NAME_MAX];
-} cutscenesaveloadallslots_t;
-
-/**
- * Starts a save-load-all-slots item: calls saveLoadAllSlots() and jumps
- * straight to the item's successMarker or failureMarker via
- * cutsceneSystemGoTo, depending on the result.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneSaveLoadAllSlotsStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a save-load-all-slots item. Never completes on its own -
- * Start already jumps to whichever marker applies before this would
- * ever run.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneSaveLoadAllSlotsUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a SAVE_LOAD_ALL_SLOTS cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneSaveLoadAllSlotsLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/story/CMakeLists.txt b/src/dusk/rpg/cutscene/item/story/CMakeLists.txt
deleted file mode 100644
index c84faff0..00000000
--- a/src/dusk/rpg/cutscene/item/story/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenestorysetflag.c
- cutscenestorywaitflag.c
-)
diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c b/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c
deleted file mode 100644
index 0939f7bc..00000000
--- a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/item/json/cutscenejsonstoryflag.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneStorySetFlagStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- storySetFlagValue(item->storySetFlag.flag, item->storySetFlag.value);
-}
-
-bool_t cutsceneStorySetFlagUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneStorySetFlagLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *flagVal = yyjson_obj_get(itemObj, "flag");
- if(!cutsceneJsonGetStoryFlag(flagVal, &item->storySetFlag.flag)) {
- errorThrow("Cutscene item (STORY_SET_FLAG): invalid or missing 'flag'");
- }
-
- item->storySetFlag.value =
- (storyflagval_t)cutsceneJsonGetInt(itemObj, "value", 1);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h b/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h
deleted file mode 100644
index 8fceb9d8..00000000
--- a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/story/story.h"
-
-typedef struct {
- storyflag_t flag;
- storyflagval_t value;
-} cutscenestorysetflag_t;
-
-/**
- * Starts a story set-flag step, immediately writing the given value to the
- * given story flag.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneStorySetFlagStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a story set-flag step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneStorySetFlagUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a STORY_SET_FLAG cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneStorySetFlagLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c b/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c
deleted file mode 100644
index df7f65e7..00000000
--- a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/item/json/cutscenejsonstoryflag.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-bool_t cutsceneStoryWaitFlagUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return storyGetFlagValue(item->storyWaitFlag.flag) == item->storyWaitFlag.value;
-}
-
-errorret_t cutsceneStoryWaitFlagLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *flagVal = yyjson_obj_get(itemObj, "flag");
- if(!cutsceneJsonGetStoryFlag(flagVal, &item->storyWaitFlag.flag)) {
- errorThrow("Cutscene item (STORY_WAIT_FLAG): invalid or missing 'flag'");
- }
-
- item->storyWaitFlag.value =
- (storyflagval_t)cutsceneJsonGetInt(itemObj, "value", 1);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h b/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h
deleted file mode 100644
index d31eeaa0..00000000
--- a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/story/story.h"
-
-typedef struct {
- storyflag_t flag;
- storyflagval_t value;
-} cutscenestorywaitflag_t;
-
-/**
- * Updates a story wait-flag step, completing once the given story flag's
- * current value matches the given value.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the flag's current value matches, false otherwise.
- */
-bool_t cutsceneStoryWaitFlagUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a STORY_WAIT_FLAG cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneStoryWaitFlagLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt
deleted file mode 100644
index 5702ce7a..00000000
--- a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- cutscenetext.c
- cutscenetextmini.c
- cutscenetextminihide.c
- cutscenefade.c
- cutsceneemoji.c
- cutsceneshake.c
- cutscenemodal.c
- cutscenemodaloptionsmarkers.c
- cutscenekeyboard.c
- cutsceneuishow.c
-)
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c b/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c
deleted file mode 100644
index 43b4466f..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "rpg/cutscene/item/json/cutscenejsonentityindex.h"
-#include "rpg/cutscene/item/json/cutscenejsonemoji.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneEmojiStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex);
- uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType);
-}
-
-bool_t cutsceneEmojiUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneEmojiLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonGetEntityIndex(
- yyjson_obj_get(itemObj, "entityIndex"), &item->emoji.entityIndex
- )) {
- errorThrow("Cutscene item (EMOJI): invalid 'entityIndex'");
- }
- item->emoji.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f);
- uiemojitype_t emojiValue;
- if(!cutsceneJsonLookupEmoji(
- yyjson_get_str(yyjson_obj_get(itemObj, "emojiType")), &emojiValue
- )) {
- errorThrow("Cutscene item (EMOJI): invalid 'emojiType'");
- }
- item->emoji.emojiType = emojiValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h b/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h
deleted file mode 100644
index 39d5fc5d..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "ui/rpg/uiemoji.h"
-
-typedef struct {
- uint8_t entityIndex;
- float_t duration;
- uiemojitype_t emojiType;
-} cutsceneemoji_t;
-
-/**
- * Starts an emoji step (shows an emoji above the entity for the given
- * duration, then completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneEmojiStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates an emoji step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneEmojiUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses an EMOJI cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneEmojiLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenefade.c b/src/dusk/rpg/cutscene/item/ui/cutscenefade.c
deleted file mode 100644
index 3b796b10..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenefade.c
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "ui/overlay/uifullbox.h"
-#include "rpg/cutscene/item/json/cutscenejsoneasing.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneFadeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uiFullboxTransition(
- &UI_FULLBOX_OVER,
- item->fade.from,
- item->fade.to,
- item->fade.duration,
- item->fade.easing
- );
-}
-
-bool_t cutsceneFadeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return !(
- UI_FULLBOX_OVER.duration > 0.0f &&
- UI_FULLBOX_OVER.time < UI_FULLBOX_OVER.duration
- );
-}
-
-errorret_t cutsceneFadeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- yyjson_val *from = yyjson_obj_get(itemObj, "from");
- yyjson_val *to = yyjson_obj_get(itemObj, "to");
- if(!yyjson_is_arr(from) || yyjson_arr_size(from) != 4 ||
- !yyjson_is_arr(to) || yyjson_arr_size(to) != 4) {
- errorThrow("Cutscene item (FADE): 'from'/'to' must each be [r, g, b, a]");
- }
- item->fade.from.r = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 0));
- item->fade.from.g = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 1));
- item->fade.from.b = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 2));
- item->fade.from.a = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 3));
- item->fade.to.r = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 0));
- item->fade.to.g = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 1));
- item->fade.to.b = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 2));
- item->fade.to.a = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 3));
- item->fade.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f);
-
- const char_t *easingName =
- yyjson_get_str(yyjson_obj_get(itemObj, "easing"));
- easingtype_t easingValue = EASING_LINEAR;
- if(easingName != NULL &&
- !cutsceneJsonLookupEasing(easingName, &easingValue)) {
- errorThrow("Cutscene item (FADE): invalid 'easing'");
- }
- item->fade.easing = easingValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenefade.h b/src/dusk/rpg/cutscene/item/ui/cutscenefade.h
deleted file mode 100644
index 21e812b7..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenefade.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "display/color.h"
-#include "animation/easing.h"
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- color_t from;
- color_t to;
- float_t duration;
- easingtype_t easing;
-} cutscenefade_t;
-
-/**
- * Starts a fade item (begins the overlay transition).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneFadeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a fade item.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the overlay transition has completed.
- */
-bool_t cutsceneFadeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a FADE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneFadeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c b/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c
deleted file mode 100644
index 79dd0323..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "ui/dialog/keyboard/uikeyboard.h"
-
-void cutsceneKeyboardStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uiKeyboardOpen(&item->keyboard.open);
-}
-
-bool_t cutsceneKeyboardUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- if(uiKeyboardIsOpen()) return false;
-
- cutsceneSystemSetTextCache(UI_KEYBOARD.text);
- return true;
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h b/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h
deleted file mode 100644
index ae97ebb2..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "ui/dialog/keyboard/uikeyboard.h"
-
-typedef struct {
- // Passed straight through to uiKeyboardOpen - see uikeyboardopen_t.
- uikeyboardopen_t open;
-} cutscenekeyboard_t;
-
-/**
- * Starts a keyboard item (opens the on-screen keyboard with the item's
- * open parameters, unmodified - see uiKeyboardOpen).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneKeyboardStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a keyboard item. Blocks the cutscene while the keyboard is
- * open; once it closes (confirmed or cancelled), caches its resulting
- * text via cutsceneSystemSetTextCache (see cutsceneSystemGetTextCache)
- * before completing, regardless of whether confirm or cancel was
- * picked - check UI_KEYBOARD.confirmed from the item's own onInput
- * callback if that distinction matters.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the keyboard has closed; false while still open.
- */
-bool_t cutsceneKeyboardUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c
deleted file mode 100644
index bad23afd..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "assert/assert.h"
-#include "ui/widget/uimodal.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneModalStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- const cutscenemodal_t *modal = &item->modal;
- assertTrue(
- modal->optionCount <= CUTSCENE_MODAL_OPTIONS_MAX,
- "Too many options for cutscene modal"
- );
-
- uiModalOpen(
- modal->title,
- modal->message,
- modal->options,
- modal->optionCount,
- modal->callback,
- NULL,
- NULL
- );
-}
-
-bool_t cutsceneModalUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- if(item->modal.optionCount == 0 && !item->modal.blocking) return true;
- return !uiModalIsOpen();
-}
-
-void cutsceneModalCloseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uiModalClose(NULL);
-}
-
-bool_t cutsceneModalCloseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneModalLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(yyjson_obj_get(itemObj, "options") != NULL) {
- errorThrow(
- "Cutscene item (MODAL): options are not supported here - use "
- "MODAL_OPTIONS_MARKERS instead"
- );
- }
- cutsceneJsonCopyString(
- itemObj, "title", "", item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS
- );
- if(!cutsceneJsonCopyString(
- itemObj, "message", NULL, item->modal.message,
- CUTSCENE_MODAL_MESSAGE_MAX_CHARS
- )) {
- errorThrow("Cutscene item (MODAL): missing/too-long 'message'");
- }
- item->modal.blocking = cutsceneJsonGetBool(itemObj, "blocking", false);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h
deleted file mode 100644
index 78df7bc6..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-#define CUTSCENE_MODAL_TITLE_MAX_CHARS 64
-#define CUTSCENE_MODAL_MESSAGE_MAX_CHARS 256
-#define CUTSCENE_MODAL_OPTIONS_MAX 4
-
-/**
- * Callback invoked with the selected option once a cutscene modal
- * item's dialog closes. Back/cancel input is disabled while it's open
- * (see uiMenuSetDisableBack), so it must be dismissed by picking one.
- *
- * @param optionIndex Index into the options array that was selected.
- * @param userData Unused - always NULL. Required by uiModalOpen's
- * callback signature.
- */
-typedef void (*cutscenemodaloptioncallback_t)(
- const uint8_t optionIndex, void *userData
-);
-
-typedef struct {
- // Display text, or a locale message ID to translate - see
- // uiModalLocalize.
- char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
- char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
-
- // NOT copied by this struct - these pointers are only read once,
- // while uiModalOpen is opening the modal in cutsceneModalStart, so
- // they only need to stay valid until then (e.g. string literals, as
- // CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
- // locale message ID, same as title/message.
- const char_t **options;
- uint8_t optionCount;
-
- cutscenemodaloptioncallback_t callback;
-
- // Whether the cutscene should wait for this dialog to close before
- // continuing. Always treated as true when optionCount > 0 (there's no
- // other way to move on). Ignored/false by default for a message-only
- // dialog, e.g. from CUTSCENE_MODAL - see cutsceneModalUpdate.
- bool_t blocking;
-} cutscenemodal_t;
-
-/**
- * Starts a modal item (shows the modal dialog with the item's title,
- * message, and options, wiring callback to fire with the selected
- * option once it closes). optionCount may be 0 for a message-only
- * dialog with no option buttons.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneModalStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a modal item.
- *
- * A message-only modal (optionCount == 0, e.g. from CUTSCENE_MODAL)
- * completes immediately unless item->modal.blocking is set, in which
- * case it waits for the dialog to close - either the player pressing
- * accept/cancel (see uiModalMessageFocusSelected) or an explicit
- * CUTSCENE_MODAL_CLOSE item elsewhere in the cutscene.
- *
- * A modal with options (optionCount > 0, e.g. from
- * CUTSCENE_MODAL_OPTIONS) always blocks regardless of the blocking
- * field - it's dismissed by the player picking one of them, and the
- * option callback is responsible for moving the cutscene on from there
- * (typically via cutsceneSystemGoTo or cutsceneRestart).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the modal has closed, or immediately for a
- * non-blocking message-only modal.
- */
-bool_t cutsceneModalUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a MODAL cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneModalLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
-
-/**
- * Starts a modal-close item (closes the currently open modal, if any).
- * Whatever should happen next belongs in the cutscene's own item
- * sequence (e.g. a CUTSCENE_CALLBACK or CUTSCENE_PRINT placed right
- * after this item), not on this item itself.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneModalCloseStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a modal-close item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneModalCloseUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-// MODAL_CLOSE has no fields of its own to parse - see cutsceneItemLoadNoop
-// (cutsceneitembase.h), used directly as its .load callback.
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c
deleted file mode 100644
index f0390a92..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "ui/widget/uimodal.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneModalOptionsMarkersCallback(
- const uint8_t optionIndex,
- void *userData
-) {
- const cutscenemodaloptionsmarkers_t *options =
- &cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
-
- // uiModalOpen disables back/cancel for a dialog with options, so
- // UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
- // the last option's marker only matters if something ever force-closes
- // the modal directly (uiModalClose) before an option is picked.
- const uint8_t index =
- optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
- cutsceneSystemGoTo(options->markers[index]);
-}
-
-void cutsceneModalOptionsMarkersStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- const cutscenemodaloptionsmarkers_t *options = &item->modalOptionsMarkers;
-
- // uiModalOpen wants an array of string pointers; options->options is a
- // 2D array of owned char buffers (see the struct's doc comment), so
- // build the pointer array it expects here.
- const char_t *optionPtrs[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
- for(uint8_t i = 0; i < options->optionCount; i++) {
- optionPtrs[i] = options->options[i];
- }
-
- uiModalOpen(
- options->title,
- options->message,
- optionPtrs,
- options->optionCount,
- cutsceneModalOptionsMarkersCallback,
- NULL,
- NULL
- );
-}
-
-bool_t cutsceneModalOptionsMarkersUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return false;
-}
-
-errorret_t cutsceneModalOptionsMarkersLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- cutsceneJsonCopyString(
- itemObj, "title", "", item->modalOptionsMarkers.title,
- CUTSCENE_MODAL_TITLE_MAX_CHARS
- );
- if(!cutsceneJsonCopyString(
- itemObj, "message", "", item->modalOptionsMarkers.message,
- CUTSCENE_MODAL_MESSAGE_MAX_CHARS
- )) {
- errorThrow(
- "Cutscene item (MODAL_OPTIONS_MARKERS): 'message' too long"
- );
- }
-
- yyjson_val *options = yyjson_obj_get(itemObj, "options");
- size_t optionCount = yyjson_arr_size(options);
- if(!yyjson_is_arr(options) || optionCount == 0 ||
- optionCount > CUTSCENE_MODAL_OPTIONS_MARKERS_MAX) {
- errorThrow(
- "Cutscene item (MODAL_OPTIONS_MARKERS): 'options' must have 1-%u "
- "entries", (uint32_t)CUTSCENE_MODAL_OPTIONS_MARKERS_MAX
- );
- }
- item->modalOptionsMarkers.optionCount = (uint8_t)optionCount;
-
- yyjson_arr_iter iter = yyjson_arr_iter_with(options);
- yyjson_val *optionVal;
- size_t o = 0;
- while((optionVal = yyjson_arr_iter_next(&iter)) != NULL) {
- if(
- !cutsceneJsonCopyString(
- optionVal, "text", NULL, item->modalOptionsMarkers.options[o],
- UI_MODAL_OPTION_TEXT_MAX
- ) ||
- !cutsceneJsonCopyString(
- optionVal, "marker", NULL, item->modalOptionsMarkers.markers[o],
- CUTSCENE_MARKER_NAME_MAX
- )
- ) {
- errorThrow(
- "Cutscene item (MODAL_OPTIONS_MARKERS): options[%u] needs "
- "'text'/'marker'", (uint32_t)o
- );
- }
- o++;
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h
deleted file mode 100644
index 9a46f178..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-#include "rpg/cutscene/item/ui/cutscenemodal.h"
-#include "rpg/cutscene/item/control/cutscenemarker.h"
-#include "ui/widget/uimodal.h"
-
-// Two options is the most either JSON MODAL_OPTIONS_MARKERS item ever
-// needs.
-#define CUTSCENE_MODAL_OPTIONS_MARKERS_MAX 2
-
-typedef struct {
- // Display text, or a locale message ID to translate - see
- // uiModalLocalize.
- char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
- char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
-
- // Each option's display text/locale message ID, and the marker to
- // cutsceneSystemGoTo when that option is selected. Copied by value at parse
- // time (see cutsceneModalOptionsMarkersLoad), so they stay valid
- // regardless of the backing JSON doc's lifetime.
- char_t options[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX][UI_MODAL_OPTION_TEXT_MAX];
- char_t markers[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX][CUTSCENE_MARKER_NAME_MAX];
- uint8_t optionCount;
-} cutscenemodaloptionsmarkers_t;
-
-/**
- * Starts a modal-options-markers item (shows the modal dialog with the
- * item's title, message, and options).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneModalOptionsMarkersStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a modal-options-markers item. Never completes on its own -
- * the cutscene blocks here indefinitely. Back/cancel input is disabled
- * while it's open (see uiMenuSetDisableBack), so it must be dismissed
- * by picking an option, which jumps straight to that option's marker
- * via cutsceneSystemGoTo.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns false always.
- */
-bool_t cutsceneModalOptionsMarkersUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a MODAL_OPTIONS_MARKERS cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneModalOptionsMarkersLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c
deleted file mode 100644
index 674286f2..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/rpgcamera.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneShakeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- rpgCameraShake(item->shake.amount, item->shake.duration);
-}
-
-bool_t cutsceneShakeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneShakeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- item->shake.amount = (uint8_t)cutsceneJsonGetInt(itemObj, "amount", 0);
- item->shake.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h
deleted file mode 100644
index 5ac06453..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t amount;
- float_t duration;
-} cutsceneshake_t;
-
-/**
- * Starts a camera shake item (kicks off the shake on the RPG camera).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneShakeStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a camera shake item. The shake itself runs asynchronously on
- * the RPG camera, so this always completes immediately.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneShakeUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a SHAKE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneShakeLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c b/src/dusk/rpg/cutscene/item/ui/cutscenetext.c
deleted file mode 100644
index 8dc5c70a..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneTextStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uiTextboxMainSetText(item->text.text);
-}
-
-bool_t cutsceneTextUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return !uiTextboxMainIsActive();
-}
-
-errorret_t cutsceneTextLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonCopyString(
- itemObj, "text", NULL, item->text.text, CUTSCENE_TEXT_MAX_CHARS
- )) {
- errorThrow("Cutscene item (TEXT): missing/too-long 'text'");
- }
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetext.h b/src/dusk/rpg/cutscene/item/ui/cutscenetext.h
deleted file mode 100644
index 6bf2bbe4..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetext.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-#define CUTSCENE_TEXT_MAX_CHARS 256
-
-typedef struct {
- char_t text[CUTSCENE_TEXT_MAX_CHARS];
-} cutscenetext_t;
-
-/**
- * Starts a text item (shows the textbox with the item's text).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneTextStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a text item.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true once the textbox has been dismissed.
- */
-bool_t cutsceneTextUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a TEXT cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneTextLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c b/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c
deleted file mode 100644
index 08bd27e9..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "ui/rpg/textbox/uitextboxminilist.h"
-#include "rpg/cutscene/item/json/cutscenejsonfield.h"
-
-void cutsceneTextMiniStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uint8_t index = uiTextboxMiniListGetNext();
- uiTextboxMiniShow(
- &UI_TEXTBOX_MINI_LIST[index],
- item->textMini.text,
- item->textMini.position,
- item->textMini.duration,
- NULL,
- NULL
- );
- CUTSCENE_SYSTEM.refs.textMiniLastCreated = index;
-}
-
-bool_t cutsceneTextMiniUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneTextMiniLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- if(!cutsceneJsonCopyString(
- itemObj, "text", NULL, item->textMini.text, CUTSCENE_TEXT_MINI_MAX_CHARS
- )) {
- errorThrow("Cutscene item (TEXT_MINI): missing/too-long 'text'");
- }
- yyjson_val *position = yyjson_obj_get(itemObj, "position");
- if(!yyjson_is_arr(position) || yyjson_arr_size(position) != 3) {
- errorThrow("Cutscene item (TEXT_MINI): 'position' must be [x, y, z]");
- }
- item->textMini.position[0] =
- (float_t)yyjson_get_num(yyjson_arr_get(position, 0));
- item->textMini.position[1] =
- (float_t)yyjson_get_num(yyjson_arr_get(position, 1));
- item->textMini.position[2] =
- (float_t)yyjson_get_num(yyjson_arr_get(position, 2));
- item->textMini.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f);
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h b/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h
deleted file mode 100644
index ad8d0731..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
-
-typedef struct {
- char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
- vec3 position;
- float_t duration;
-} cutscenetextmini_t;
-
-/**
- * Starts a mini text item (shows a mini textbox at the given world
- * position for the given duration, then completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneTextMiniStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a mini text item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneTextMiniUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a TEXT_MINI cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneTextMiniLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c b/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c
deleted file mode 100644
index b4df6066..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "ui/rpg/textbox/uitextboxminilist.h"
-
-void cutsceneTextMiniHideStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
- uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
-}
-
-bool_t cutsceneTextMiniHideUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneTextMiniHideLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- item->textMiniHide.index =
- (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "index"));
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h b/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h
deleted file mode 100644
index 68f8f55d..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-typedef struct {
- uint8_t index;
-} cutscenetextminihide_t;
-
-/**
- * Starts a mini text hide step (closes the mini textbox immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneTextMiniHideStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a mini text hide step (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneTextMiniHideUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a TEXT_MINI_HIDE cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneTextMiniHideLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c b/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c
deleted file mode 100644
index 3ec69432..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/cutscene/item/cutsceneitem.h"
-#include "ui/screen/mainmenu/uimainmenu.h"
-#include "rpg/cutscene/item/json/cutscenejsonuiscreentype.h"
-
-void cutsceneUIShowStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- switch(item->uiShow.screen) {
- case UI_SCREEN_TYPE_MAIN_MENU:
- uiMainMenuOpen();
- break;
- }
-}
-
-bool_t cutsceneUIShowUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-) {
- return true;
-}
-
-errorret_t cutsceneUIShowLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-) {
- uiscreentype_t screenValue;
- if(!cutsceneJsonLookupUiScreenType(
- yyjson_get_str(yyjson_obj_get(itemObj, "name")), &screenValue
- )) {
- errorThrow("Cutscene item (UI_SHOW): invalid 'name'");
- }
- item->uiShow.screen = screenValue;
-
- errorOk();
-}
diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h b/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h
deleted file mode 100644
index 17f5ea8d..00000000
--- a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/cutscene/item/cutsceneitembase.h"
-
-// Full-screen UI panels a cutscene can declaratively open via UI_SHOW - not
-// modals/dialogs (those already have their own dedicated item types, e.g.
-// MODAL/MODAL_OPTIONS_MARKERS/KEYBOARD), just whole-screen views like the
-// main menu. Add a case here (and to cutsceneUIShowStart) as more screens
-// need to be openable this way.
-typedef enum {
- UI_SCREEN_TYPE_MAIN_MENU,
-} uiscreentype_t;
-
-typedef struct {
- uiscreentype_t screen;
-} cutsceneuishow_t;
-
-/**
- * Starts a UI_SHOW item, opening the requested full-screen UI panel.
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- */
-void cutsceneUIShowStart(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Updates a UI_SHOW item (always completes immediately).
- *
- * @param item The cutscene item.
- * @param data Runtime data storage.
- * @returns true always.
- */
-bool_t cutsceneUIShowUpdate(
- const cutsceneitem_t *item,
- cutsceneitemdata_t *data
-);
-
-/**
- * Parses a UI_SHOW cutscene item's JSON fields into item.
- *
- * @param itemObj The item's JSON object.
- * @param item Destination item, already zeroed by the caller.
- * @return Error code indicating success or failure of the parse.
- */
-errorret_t cutsceneUIShowLoad(
- yyjson_val *itemObj,
- cutsceneitem_t *item
-);
diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt
deleted file mode 100644
index da55a228..00000000
--- a/src/dusk/rpg/entity/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- entity.c
- entitydir.c
- entitypathstep.c
- player.c
-)
-
-add_subdirectory(anim)
-add_subdirectory(interact)
-add_subdirectory(npc)
-add_subdirectory(item)
-add_subdirectory(global)
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/anim/CMakeLists.txt b/src/dusk/rpg/entity/anim/CMakeLists.txt
deleted file mode 100644
index 54c1015e..00000000
--- a/src/dusk/rpg/entity/anim/CMakeLists.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- entityanim.c
- entityanimidle.c
- entityanimturn.c
- entityanimwalk.c
- entityanimrun.c
-)
diff --git a/src/dusk/rpg/entity/anim/entityanim.c b/src/dusk/rpg/entity/anim/entityanim.c
deleted file mode 100644
index fa98628f..00000000
--- a/src/dusk/rpg/entity/anim/entityanim.c
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/tile.h"
-#include "time/time.h"
-#include "entityanimwalk.h"
-
-const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
- [ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
- [ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
- [ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
- [ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
-};
-
-float_t entityAnimTileZOffset(const worldpos_t pos) {
- return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
-}
-
-void entityAnimUpdate(entity_t *entity) {
- if(entity->animation != ENTITY_ANIM_IDLE) {
- entity->animTime -= TIME.delta;
- if(entity->animTime <= 0) {
- if(
- entity->animation == ENTITY_ANIM_WALK ||
- entity->animation == ENTITY_ANIM_RUN
- ) {
- entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
- }
- entity->animation = ENTITY_ANIM_IDLE;
- entity->animTime = 0;
- }
- }
- if(entity->walkEndCooldown > 0) {
- entity->walkEndCooldown -= TIME.delta;
- if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
- }
- ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
-}
diff --git a/src/dusk/rpg/entity/anim/entityanim.h b/src/dusk/rpg/entity/anim/entityanim.h
deleted file mode 100644
index ff4cde20..00000000
--- a/src/dusk/rpg/entity/anim/entityanim.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "entityanimidle.h"
-#include "entityanimturn.h"
-#include "entityanimwalk.h"
-#include "entityanimrun.h"
-
-typedef struct entity_s entity_t;
-
-typedef enum {
- ENTITY_ANIM_IDLE,
- ENTITY_ANIM_TURN,
- ENTITY_ANIM_WALK,
- ENTITY_ANIM_RUN,
- ENTITY_ANIM_COUNT
-} entityanim_t;
-
-typedef struct {
- /** Updates the render position for this animation state. */
- void (*update)(entity_t *entity);
-} entityanimcallback_t;
-
-extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
-
-/**
- * Updates the entity animation timer and render position.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityAnimUpdate(entity_t *entity);
-
-/**
- * Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
- * Used to lift entity render position to mid-ramp height.
- *
- * @param pos World position to sample.
- * @returns float_t The Z offset to apply.
- */
-float_t entityAnimTileZOffset(const worldpos_t pos);
diff --git a/src/dusk/rpg/entity/anim/entityanimidle.c b/src/dusk/rpg/entity/anim/entityanimidle.c
deleted file mode 100644
index fb9f5d4a..00000000
--- a/src/dusk/rpg/entity/anim/entityanimidle.c
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "entityanim.h"
-
-void entityAnimIdleUpdate(entity_t *entity) {
- entity->renderPosition[0] = (float_t)entity->position.x;
- entity->renderPosition[1] = (float_t)entity->position.y;
- entity->renderPosition[2] = (
- (float_t)entity->position.z + entityAnimTileZOffset(entity->position)
- ) * WORLD_LAYER_HEIGHT;
-}
diff --git a/src/dusk/rpg/entity/anim/entityanimidle.h b/src/dusk/rpg/entity/anim/entityanimidle.h
deleted file mode 100644
index b7bd9a53..00000000
--- a/src/dusk/rpg/entity/anim/entityanimidle.h
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct entity_s entity_t;
-
-/**
- * Updates render position for the idle animation state.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityAnimIdleUpdate(entity_t *entity);
diff --git a/src/dusk/rpg/entity/anim/entityanimrun.c b/src/dusk/rpg/entity/anim/entityanimrun.c
deleted file mode 100644
index 27a32c2c..00000000
--- a/src/dusk/rpg/entity/anim/entityanimrun.c
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "entityanim.h"
-
-void entityAnimRunUpdate(entity_t *entity) {
- float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
- float_t zFrom = (float_t)entity->lastPosition.z
- + entityAnimTileZOffset(entity->lastPosition);
- float_t zTo = (float_t)entity->position.z
- + entityAnimTileZOffset(entity->position);
- entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
- (float_t)entity->position.x - (float_t)entity->lastPosition.x
- );
- entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
- (float_t)entity->position.y - (float_t)entity->lastPosition.y
- );
- entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
-}
diff --git a/src/dusk/rpg/entity/anim/entityanimrun.h b/src/dusk/rpg/entity/anim/entityanimrun.h
deleted file mode 100644
index ecace434..00000000
--- a/src/dusk/rpg/entity/anim/entityanimrun.h
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "time/time.h"
-
-typedef struct entity_s entity_t;
-
-#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
-
-/**
- * Updates render position for the run animation state.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityAnimRunUpdate(entity_t *entity);
diff --git a/src/dusk/rpg/entity/anim/entityanimturn.c b/src/dusk/rpg/entity/anim/entityanimturn.c
deleted file mode 100644
index d31ad814..00000000
--- a/src/dusk/rpg/entity/anim/entityanimturn.c
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "entityanim.h"
-
-void entityAnimTurnUpdate(entity_t *entity) {
- entity->renderPosition[0] = (float_t)entity->position.x;
- entity->renderPosition[1] = (float_t)entity->position.y;
- entity->renderPosition[2] = (
- (float_t)entity->position.z + entityAnimTileZOffset(entity->position)
- ) * WORLD_LAYER_HEIGHT;
-}
diff --git a/src/dusk/rpg/entity/anim/entityanimturn.h b/src/dusk/rpg/entity/anim/entityanimturn.h
deleted file mode 100644
index 33368d52..00000000
--- a/src/dusk/rpg/entity/anim/entityanimturn.h
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "time/time.h"
-
-typedef struct entity_s entity_t;
-
-#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
-
-/**
- * Updates render position for the turn animation state.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityAnimTurnUpdate(entity_t *entity);
diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.c b/src/dusk/rpg/entity/anim/entityanimwalk.c
deleted file mode 100644
index e8aa2833..00000000
--- a/src/dusk/rpg/entity/anim/entityanimwalk.c
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "entityanim.h"
-
-void entityAnimWalkUpdate(entity_t *entity) {
- float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
- float_t zFrom = (float_t)entity->lastPosition.z
- + entityAnimTileZOffset(entity->lastPosition);
- float_t zTo = (float_t)entity->position.z
- + entityAnimTileZOffset(entity->position);
- entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
- (float_t)entity->position.x - (float_t)entity->lastPosition.x
- );
- entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
- (float_t)entity->position.y - (float_t)entity->lastPosition.y
- );
- entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
-}
diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.h b/src/dusk/rpg/entity/anim/entityanimwalk.h
deleted file mode 100644
index 5ab12585..00000000
--- a/src/dusk/rpg/entity/anim/entityanimwalk.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "time/time.h"
-
-typedef struct entity_s entity_t;
-
-#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
-#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
-
-/**
- * Updates render position for the walk animation state.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityAnimWalkUpdate(entity_t *entity);
diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c
deleted file mode 100644
index dabe2f67..00000000
--- a/src/dusk/rpg/entity/entity.c
+++ /dev/null
@@ -1,333 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entity.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "util/math.h"
-#include "console/console.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/maparea.h"
-#include "rpg/overworld/chunk.h"
-#include "rpg/overworld/tile.h"
-
-entity_t ENTITIES[ENTITY_COUNT];
-
-void entityInit(entity_t *entity, const entitytype_t type) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type");
- assertTrue(type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
- assertTrue(
- entity >= ENTITIES && entity < ENTITIES + ENTITY_COUNT,
- "Entity pointer is out of bounds"
- );
-
- memoryZero(entity, sizeof(entity_t));
- entity->id = (uint8_t)(entity - ENTITIES);
- entity->globalId = ENTITY_GLOBAL_ID_NULL;
- entity->type = type;
- entity->chunkIndex = -1;
-
- if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
-}
-
-void entityUpdate(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
- assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
-
- // What state is the entity in?
- entityAnimUpdate(entity);
-
- // Movement code.
- if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
- ENTITY_CALLBACKS[entity->type].movement(entity);
- }
-}
-
-bool_t entityCanTurn(entity_t *entity) {
- return entity->animation == ENTITY_ANIM_IDLE &&
- entity->walkEndCooldown <= 0;
-}
-
-bool_t entityCanWalk(entity_t *entity) {
- return entity->animation == ENTITY_ANIM_IDLE;
-}
-
-bool_t entityCanRun(entity_t *entity) {
- return entity->animation == ENTITY_ANIM_IDLE;
-}
-
-bool_t entityCanUnload(entity_t *entity) {
- return entity->globalId < ENTITY_GLOBAL_ID_START;
-}
-
-void entityTurn(entity_t *entity, const entitydir_t direction) {
- if(!entityCanTurn(entity)) return;
- entity->direction = direction;
- entity->animation = ENTITY_ANIM_TURN;
- entity->animTime = ENTITY_ANIM_TURN_DURATION;
-}
-
-bool_t entityWalkCheckRampUp(
- const tile_t tileCurrent,
- const entitydir_t direction,
- const worldpos_t newPos,
- tile_t *tileNew
-) {
- if(!tileShapeIsRamp(tileCurrent.shape)) return false;
-
- bool_t facingRamp =
- // Can only walk UP the direction the ramp faces.
- (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
- // If diagonal ramp, can go up one of two ways only. Inner ramps
- // share the same allowed directions as their outer counterparts.
- (
- (
- tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
- tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
- ) &&
- (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
- ) ||
- (
- (
- tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
- tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
- ) &&
- (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
- ) ||
- (
- (
- tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
- tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
- ) &&
- (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
- ) ||
- (
- (
- tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
- tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
- ) &&
- (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
- );
- if(!facingRamp) return false;
-
- tile_t tileNewSaved = *tileNew;
- *tileNew = TILE_NULL;
- worldpos_t abovePos = newPos;
- abovePos.z += 1;
- tile_t tileAbove = mapGetTile(abovePos);
-
- if(
- tileAbove.shape != TILE_SHAPE_NULL && tileShapeIsWalkable(tileAbove.shape)
- ) {
- return true;
- }
-
- *tileNew = tileNewSaved;
- return false;
-}
-
-bool_t entityWalkCheckFall(
- const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction
-) {
- if(tileNew.shape != TILE_SHAPE_NULL || newPos.z <= 0) return false;
-
- worldpos_t belowPos = newPos;
- belowPos.z -= 1;
- tile_t tileBelow = mapGetTile(belowPos);
- if(tileBelow.shape == TILE_SHAPE_NULL || !tileShapeIsRamp(tileBelow.shape)) {
- return false;
- }
-
- return (
- // This handles regular cardinal ramps
- (entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) ==
- tileBelow.shape ||
- // This handles diagonal ramps. Inner ramps share the same
- // allowed directions as their outer counterparts.
- (
- (
- tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
- tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
- ) &&
- (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
- ) ||
- (
- (
- tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
- tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
- ) &&
- (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
- ) ||
- (
- (
- tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
- tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
- ) &&
- (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
- ) ||
- (
- (
- tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
- tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
- ) &&
- (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
- )
- );
-}
-
-bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos) {
- entity_t *other = ENTITIES;
- do {
- if(other == entity) continue;
- if(other->type == ENTITY_TYPE_NULL) continue;
- if(!worldPosIsEqual(other->position, newPos)) continue;
- return true;
- } while(++other, other < &ENTITIES[ENTITY_COUNT]);
-
- return false;
-}
-
-void entityWalk(entity_t *entity, const entitydir_t direction) {
- if(!entityCanWalk(entity)) return;
- // TODO: Animation, delay, etc.
- entity->direction = direction;
-
- // Where are we moving?
- worldpos_t newPos = entity->position;
- worldunits_t relX, relY;
- {
- entityDirGetRelative(direction, &relX, &relY);
- newPos.x += relX;
- newPos.y += relY;
- }
-
- // Get tile under foot
- tile_t tileCurrent = mapGetTile(entity->position);
- tile_t tileNew = mapGetTile(newPos);
-
- bool_t raise =
- entityWalkCheckRampUp(tileCurrent, direction, newPos, &tileNew);
- bool_t fall = !raise && entityWalkCheckFall(tileNew, newPos, direction);
-
- // Can we walk here?
- if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
-
- // Raise/fall must be applied before checking for blocking entities,
- // otherwise the check compares against the wrong z-level.
- if(raise) {
- newPos.z += 1;
- } else if(fall) {
- newPos.z -= 1;
- }
-
- // Entity in way?
- if(entityWalkIsBlockedByEntity(entity, newPos)) return;// Blocked
-
- entity->lastPosition = entity->position;
- entity->position = newPos;
- entity->animation = ENTITY_ANIM_WALK;
- entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
- entityUpdateChunk(entity);
- mapAreaCheckEntity(entity);
-}
-
-void entityRun(entity_t *entity, const entitydir_t direction) {
- if(!entityCanRun(entity)) return;
- entityWalk(entity, direction);
- if(entity->animation == ENTITY_ANIM_WALK) {
- entity->animation = ENTITY_ANIM_RUN;
- entity->animTime = ENTITY_ANIM_RUN_DURATION;
- }
-}
-
-entity_t * entityGetAt(const worldpos_t position) {
- entity_t *ent = ENTITIES;
- do {
- if(ent->type == ENTITY_TYPE_NULL) continue;
- if(!worldPosIsEqual(ent->position, position)) continue;
- return ent;
- } while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
-
- return NULL;
-}
-
-entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
- entity_t *ent = ENTITIES;
- do {
- if(ent->type == ENTITY_TYPE_NULL) continue;
- if(ent->globalId != globalId) continue;
- return ent;
- } while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
-
- return NULL;
-}
-
-uint8_t entityGetAvailable() {
- entity_t *ent = ENTITIES;
- do {
- if(ent->type == ENTITY_TYPE_NULL) return ent - ENTITIES;
- } while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
-
- return 0xFF;
-}
-
-void entityPositionSet(entity_t *entity, const worldpos_t pos) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- entity->lastPosition = pos;
- entity->position = pos;
- entity->animation = ENTITY_ANIM_IDLE;
- entity->animTime = 0;
- entity->walkEndCooldown = 0;
- entityUpdateChunk(entity);
-}
-
-void entitySetChunk(entity_t *entity, const chunkindex_t chunkIndex) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
-
- chunk_t *old = mapGetChunk(entity->chunkIndex);
- if(old != NULL) {
- for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
- if(old->entities[i] != entity->id) continue;
- old->entities[i] = 0xFF;
- }
- }
-
- // Only claim the new chunk once actually inserted into one of its slots -
- // otherwise entity->chunkIndex would point at a chunk that doesn't know
- // about this entity, so it would never be torn down on unload.
- entity->chunkIndex = CHUNK_INDEX_INVALID;
-
- chunk_t *next = mapGetChunk(chunkIndex);
- if(next != NULL) {
- for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
- if(next->entities[i] != 0xFF) continue;
- next->entities[i] = entity->id;
- entity->chunkIndex = chunkIndex;
- break;
- }
-
- if(entity->chunkIndex != chunkIndex) {
- consolePrint(
- "entitySetChunk: chunk %d has no free entity slots, entity %u "
- "left untracked",
- chunkIndex, entity->id
- );
- }
- }
-}
-
-void entityUpdateChunk(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
-
- chunkpos_t cp;
- worldPosToChunkPos(&entity->position, &cp);
- chunkindex_t ci = mapGetChunkIndexAt(cp);
- entitySetChunk(entity, ci);
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h
deleted file mode 100644
index 2ef8b64c..00000000
--- a/src/dusk/rpg/entity/entity.h
+++ /dev/null
@@ -1,215 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "entitydir.h"
-#include "anim/entityanim.h"
-#include "interact/entityinteract.h"
-#include "entitytype.h"
-#include "npc/npc.h"
-#include "rpg/overworld/tile.h"
-
-typedef struct map_s map_t;
-
-typedef uint16_t entityglobalid_t;
-
-#define ENTITY_GLOBAL_ID_NULL 0
-#define ENTITY_GLOBAL_ID_START 1
-#define ENTITY_GLOBAL_ID_PLAYER 1
-
-typedef struct entity_s {
- uint8_t id;
- entityglobalid_t globalId;
- entitytype_t type;
- entitytypedata_t data;
-
- // Movement
- entitydir_t direction;
- worldpos_t position;
- worldpos_t lastPosition;
- vec3 renderPosition;
-
- entityanim_t animation;
- float_t animTime;
- float_t walkEndCooldown;
-
- entityinteract_t interact;
-
- chunkindex_t chunkIndex;
-} entity_t;
-
-extern entity_t ENTITIES[ENTITY_COUNT];
-
-/**
- * Initializes an entity structure.
- *
- * @param entity Pointer to the entity structure to initialize.
- * @param type The type of the entity.
- */
-void entityInit(entity_t *entity, const entitytype_t type);
-
-/**
- * Updates an entity.
- *
- * @param entity Pointer to the entity structure to update.
- */
-void entityUpdate(entity_t *entity);
-
-/**
- * Returns true if the entity is in a state where it can turn.
- *
- * @param entity Pointer to the entity to check.
- * @returns True if the entity can turn.
- */
-bool_t entityCanTurn(entity_t *entity);
-
-/**
- * Returns true if the entity is in a state where it can walk.
- *
- * @param entity Pointer to the entity to check.
- * @returns True if the entity can walk.
- */
-bool_t entityCanWalk(entity_t *entity);
-
-/**
- * Returns true if the entity is in a state where it can run.
- *
- * @param entity Pointer to the entity to check.
- * @returns True if the entity can run.
- */
-bool_t entityCanRun(entity_t *entity);
-
-/**
- * Returns true if the entity is allowed to be unloaded. By default this is
- * true for entities whose global ID falls within the randomly assigned
- * range below ENTITY_GLOBAL_ID_START.
- *
- * @param entity Pointer to the entity to check.
- * @returns True if the entity can be unloaded.
- */
-bool_t entityCanUnload(entity_t *entity);
-
-/**
- * Turn an entity to face a new direction.
- *
- * @param entity Pointer to the entity to turn.
- * @param direction The direction to face.
- */
-void entityTurn(entity_t *entity, const entitydir_t direction);
-
-/**
- * Checks whether an entity standing on a ramp tile can walk up it towards
- * newPos. If so, tileNew is cleared to TILE_NULL so the caller treats the
- * move as a raise rather than a normal walk onto whatever tile is there;
- * if the tile above isn't walkable, tileNew is restored to its original
- * value.
- *
- * @param tileCurrent The tile the entity is currently standing on.
- * @param direction The direction the entity is moving in.
- * @param newPos The world position the entity is moving to.
- * @param tileNew In/out: the tile at newPos, possibly cleared to TILE_NULL.
- * @returns True if the entity should be raised up onto the ramp.
- */
-bool_t entityWalkCheckRampUp(
- const tile_t tileCurrent,
- const entitydir_t direction,
- const worldpos_t newPos,
- tile_t *tileNew
-);
-
-/**
- * Checks whether an entity moving onto an empty tile should fall down onto
- * a ramp one z-level below that faces back towards where it came from.
- *
- * @param tileNew The tile at the position the entity is moving to.
- * @param newPos The world position the entity is moving to.
- * @param direction The direction the entity is moving in.
- * @returns True if the entity should fall to the tile below newPos.
- */
-bool_t entityWalkCheckFall(
- const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction
-);
-
-/**
- * Checks whether any other loaded entity already occupies a world position.
- *
- * @param entity The entity that is moving (excluded from the check).
- * @param newPos The world position to check for occupancy.
- * @returns True if another entity occupies newPos.
- */
-bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos);
-
-/**
- * Make an entity walk in a direction.
- *
- * @param entity Pointer to the entity to make walk.
- * @param direction The direction to walk in.
- */
-void entityWalk(entity_t *entity, const entitydir_t direction);
-
-/**
- * Make an entity run in a direction.
- *
- * @param entity Pointer to the entity to make run.
- * @param direction The direction to run in.
- */
-void entityRun(entity_t *entity, const entitydir_t direction);
-
-/**
- * Gets the entity at a specific world position.
- *
- * @param map Pointer to the map to check.
- * @param pos The world position to check.
- * @return Pointer to the entity at the position, or NULL if none.
- */
-entity_t *entityGetAt(const worldpos_t pos);
-
-/**
- * Gets the entity with the given global ID, if one is currently loaded.
- *
- * @param globalId The global ID to search for.
- * @return Pointer to the matching entity, or NULL if none is loaded.
- */
-entity_t *entityGetByGlobalId(const entityglobalid_t globalId);
-
-/**
- * Gets an available entity index.
- *
- * @return The index of an available entity, or 0xFF if none are available.
- */
-uint8_t entityGetAvailable();
-
-/**
- * Assigns an entity to a chunk, removing it from its current chunk first.
- * Pass CHUNK_INDEX_INVALID as chunkIndex to detach the entity from any
- * chunk. If the target chunk has no free entity slots, the entity is
- * left detached (chunkIndex CHUNK_INDEX_INVALID) rather than assigned to
- * a chunk that isn't actually tracking it - entityUpdateChunk will keep
- * retrying on subsequent moves.
- *
- * @param entity Pointer to the entity.
- * @param chunkIndex Index of the chunk to assign to, or
- * CHUNK_INDEX_INVALID for none.
- */
-void entitySetChunk(entity_t *entity, const chunkindex_t chunkIndex);
-
-/**
- * Resolves the chunk that an entity's current position falls into and
- * assigns the entity to it via entitySetChunk. Leaves the entity's chunk
- * unchanged if its position doesn't fall within any loaded chunk.
- *
- * @param entity Pointer to the entity to update.
- */
-void entityUpdateChunk(entity_t *entity);
-
-/**
- * Instantly moves an entity to a world position, resetting movement state.
- *
- * @param entity Pointer to the entity to move.
- * @param pos The world position to place the entity at.
- */
-void entityPositionSet(entity_t *entity, const worldpos_t pos);
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/entitydir.c b/src/dusk/rpg/entity/entitydir.c
deleted file mode 100644
index bf75e7c7..00000000
--- a/src/dusk/rpg/entity/entitydir.c
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entitydir.h"
-#include "assert/assert.h"
-
-entitydir_t entityDirGetOpposite(const entitydir_t dir) {
- switch(dir) {
- case ENTITY_DIR_NORTH: return ENTITY_DIR_SOUTH;
- case ENTITY_DIR_SOUTH: return ENTITY_DIR_NORTH;
- case ENTITY_DIR_EAST: return ENTITY_DIR_WEST;
- case ENTITY_DIR_WEST: return ENTITY_DIR_EAST;
- default: return dir;
- }
-}
-
-void entityDirGetRelative(
- const entitydir_t from,
- worldunits_t *outX,
- worldunits_t *outY
-) {
- assertValidEntityDir(from, "Invalid direction provided");
- assertNotNull(outX, "Output X pointer cannot be NULL");
- assertNotNull(outY, "Output Y pointer cannot be NULL");
-
- switch(from) {
- case ENTITY_DIR_NORTH:
- *outX = 0;
- *outY = 1;
- break;
-
- case ENTITY_DIR_EAST:
- *outX = 1;
- *outY = 0;
- break;
-
- case ENTITY_DIR_SOUTH:
- *outX = 0;
- *outY = -1;
- break;
-
- case ENTITY_DIR_WEST:
- *outX = -1;
- *outY = 0;
- break;
- }
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/entitydir.h b/src/dusk/rpg/entity/entitydir.h
deleted file mode 100644
index 2eefab88..00000000
--- a/src/dusk/rpg/entity/entitydir.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/worldpos.h"
-
-typedef enum {
- ENTITY_DIR_NORTH,
- ENTITY_DIR_EAST,
- ENTITY_DIR_SOUTH,
- ENTITY_DIR_WEST,
- ENTITY_DIR_UP = ENTITY_DIR_NORTH,
- ENTITY_DIR_DOWN = ENTITY_DIR_SOUTH,
- ENTITY_DIR_LEFT = ENTITY_DIR_WEST,
- ENTITY_DIR_RIGHT = ENTITY_DIR_EAST,
-} entitydir_t;
-
-/**
- * Gets the opposite direction of a given direction.
- *
- * @param dir The direction to get the opposite of.
- * @return entitydir_t The opposite direction.
- */
-entitydir_t entityDirGetOpposite(const entitydir_t dir);
-
-/**
- * Asserts a given direction is valid.
- *
- * @param dir The direction to validate.
- * @param msg The message to display if the assertion fails.
- */
-#define assertValidEntityDir(dir, msg) \
- assertTrue( \
- (dir) == ENTITY_DIR_NORTH || \
- (dir) == ENTITY_DIR_EAST || \
- (dir) == ENTITY_DIR_SOUTH || \
- (dir) == ENTITY_DIR_WEST, \
- msg \
- )
-
-/**
- * Gets the relative x and y offsets for a given direction.
- *
- * @param dir The direction to get offsets for.
- * @param relX Pointer to store the relative x offset.
- * @param relY Pointer to store the relative y offset.
- */
-void entityDirGetRelative(
- const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/entitypathstep.c b/src/dusk/rpg/entity/entitypathstep.c
deleted file mode 100644
index bd474415..00000000
--- a/src/dusk/rpg/entity/entitypathstep.c
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entitypathstep.h"
-#include "entitydir.h"
-#include "assert/assert.h"
-
-bool_t entityPathStep(
- entity_t *entity,
- const worldpos_t target,
- bool_t walkAround
-) {
- assertNotNull(entity, "Entity must not be NULL");
- assertTrue(
- entity->type != ENTITY_TYPE_NULL,
- "Cannot path step a NULL entity type"
- );
- assertTrue(
- entity >= ENTITIES && entity < ENTITIES + ENTITY_COUNT,
- "Entity pointer is out of bounds"
- );
-
- if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
- return true;
- }
- if(!entityCanWalk(entity)) return false;
-
- entitydir_t dir;
- if(entity->position.x != target.x) {
- dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
- } else if(entity->position.y != target.y) {
- dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
- } else {
- dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
- }
-
- entityWalk(entity, dir);
-
- if(walkAround && entity->animation == ENTITY_ANIM_IDLE) {
- // Primary direction blocked - try perpendicular axes.
- entitydir_t alt, altOpp;
- if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) {
- alt = entity->position.y <= target.y
- ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
- } else {
- alt = entity->position.x <= target.x
- ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
- }
- altOpp = entityDirGetOpposite(alt);
- entityWalk(entity, alt);
- if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp);
- }
-
- return false;
-}
diff --git a/src/dusk/rpg/entity/entitypathstep.h b/src/dusk/rpg/entity/entitypathstep.h
deleted file mode 100644
index 1608cae8..00000000
--- a/src/dusk/rpg/entity/entitypathstep.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "entity.h"
-
-/**
- * Attempts to walk the entity one tile toward target. Prefers resolving X
- * first, then Y, then Z. Does nothing if the entity cannot currently walk.
- * When walkAround is true and the preferred direction is blocked by an entity,
- * tries perpendicular directions to navigate around it.
- *
- * @param entity Pointer to the entity to move.
- * @param target The world position to move toward.
- * @param walkAround Whether to try perpendicular directions when blocked.
- * @returns true if the entity is already at target, false otherwise.
- */
-bool_t entityPathStep(
- entity_t *entity,
- const worldpos_t target,
- bool_t walkAround
-);
diff --git a/src/dusk/rpg/entity/entitytype.h b/src/dusk/rpg/entity/entitytype.h
deleted file mode 100644
index 6b7d2769..00000000
--- a/src/dusk/rpg/entity/entitytype.h
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/entity/player.h"
-#include "npc/npc.h"
-#include "item/entityitem.h"
-
-typedef enum {
- ENTITY_TYPE_NULL,
-
- ENTITY_TYPE_PLAYER,
- ENTITY_TYPE_NPC,
- ENTITY_TYPE_ITEM,
-
- ENTITY_TYPE_COUNT
-} entitytype_enum_t;
-
-typedef uint8_t entitytype_t;
-
-typedef union {
- player_t player;
- npc_t npc;
- entityitem_t item;
-} entitytypedata_t;
-
-typedef struct {
- /**
- * Initialization callback for the entity type.
- * @param entity Pointer to the entity to initialize.
- */
- void (*init)(entity_t *entity);
-
- /**
- * Movement callback for the entity type. Gated by cutscene input.
- * @param entity Pointer to the entity to move.
- */
- void (*movement)(entity_t *entity);
-} entitycallback_t;
-
-static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
- [ENTITY_TYPE_NULL] = { NULL },
-
- [ENTITY_TYPE_PLAYER] = {
- .init = playerInit,
- .movement = playerInput
- },
-
- [ENTITY_TYPE_NPC] = {
- .init = npcInit,
- .movement = npcMovement,
- },
-
- [ENTITY_TYPE_ITEM] = {
- .init = entityItemInit,
- .movement = entityItemMovement,
- }
-};
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/global/CMakeLists.txt b/src/dusk/rpg/entity/global/CMakeLists.txt
deleted file mode 100644
index 9b35b01d..00000000
--- a/src/dusk/rpg/entity/global/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
-)
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/global/entityglobal.h b/src/dusk/rpg/entity/global/entityglobal.h
deleted file mode 100644
index 10d51d60..00000000
--- a/src/dusk/rpg/entity/global/entityglobal.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "entityglobaldefs.h"
-#include "entitygloballist.h"
-
-#define ENTITY_GLOBAL_LIST_COUNT ( \
- sizeof(ENTITY_GLOBAL_LIST) / \
- sizeof(ENTITY_GLOBAL_LIST[0]) \
-)
-
-//EOF
diff --git a/src/dusk/rpg/entity/global/entityglobaldefs.h b/src/dusk/rpg/entity/global/entityglobaldefs.h
deleted file mode 100644
index 73913e45..00000000
--- a/src/dusk/rpg/entity/global/entityglobaldefs.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "rpg/overworld/worldpos.h"
-#include "rpg/entity/entity.h"
-
-typedef struct entity_s entity_t;
-
-typedef struct {
- entity_t *entity;
- worldpos_t position;
-} entityglobalcreate_t;
-
-/**
- * Callback invoked to initialize a global entity.
- *
- * @param create Pointer to the entity/position being initialized.
- * @returns An error code.
- */
-typedef void (*entityglobalinitcallback_t)(
- entityglobalcreate_t *create
-);
-
-typedef struct {
- entitytype_t type;
- entityglobalinitcallback_t callback;
-} entityglobaldef_t;
-
-#define ENTITY_GLOBAL(id, entType, callbackFn) \
- [id] = { .type = entType, .callback = callbackFn }
-
-#define ENTITY_GLOBAL_CALLBACK(id) \
- static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create)
-
-#define ENTITY_GLOBAL_REF(id) \
- ENTTIYT_GLOBAL_CALLBACK_##id
-
-//EOF
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/global/entitygloballist.h b/src/dusk/rpg/entity/global/entitygloballist.h
deleted file mode 100644
index c2a83e79..00000000
--- a/src/dusk/rpg/entity/global/entitygloballist.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "entityglobaldefs.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-
-ENTITY_GLOBAL_CALLBACK(3) {
- create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH;
- npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
- npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
- npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
- npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
-}
-
-ENTITY_GLOBAL_CALLBACK(4) {
- create->entity->interact.type = ENTITY_INTERACT_CUTSCENE_NAME;
- create->entity->interact.data.cutsceneName = "cutscenes/battletest.jsonc";
-}
-
-static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
- ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL),
- ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL),
-
- ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)),
- ENTITY_GLOBAL(4, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(4)),
-};
-
-//EOF
diff --git a/src/dusk/rpg/entity/interact/CMakeLists.txt b/src/dusk/rpg/entity/interact/CMakeLists.txt
deleted file mode 100644
index f7e07e35..00000000
--- a/src/dusk/rpg/entity/interact/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- entityinteract.c
-)
diff --git a/src/dusk/rpg/entity/interact/entityinteract.c b/src/dusk/rpg/entity/interact/entityinteract.c
deleted file mode 100644
index 063466c0..00000000
--- a/src/dusk/rpg/entity/interact/entityinteract.c
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "assert/assert.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-
-void entityInteractWith(entity_t *player, entity_t *target) {
- assertNotNull(player, "Player entity pointer cannot be NULL");
- assertNotNull(target, "Target entity pointer cannot be NULL");
-
- switch(target->interact.type) {
- case ENTITY_INTERACT_CUTSCENE:
- assertNotNull(
- target->interact.data.cutscene,
- "Interact cutscene pointer cannot be NULL"
- );
- cutsceneSystemStartCutsceneWith(
- target->interact.data.cutscene,
- player,
- target
- );
- break;
-
- case ENTITY_INTERACT_CUTSCENE_NAME: {
- cutscene_t *cutscene =
- cutsceneCutsceneResolve(target->interact.data.cutsceneName);
- if(cutscene != NULL) {
- cutsceneSystemStartCutsceneWith(cutscene, player, target);
- }
- break;
- }
-
- case ENTITY_INTERACT_PRINT:
- uiTextboxMainSetText(target->interact.data.message);
-
- // If NPC turn to face player.
- if(target->type == ENTITY_TYPE_NPC) {
- target->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
- target->animation = ENTITY_ANIM_IDLE;
- entityTurn(target, entityDirGetOpposite(player->direction));
- }
-
- // entityTurn(player, player->direction); // Redundant (for now)
- break;
-
- case ENTITY_INTERACT_CALLBACK:
- assertNotNull(
- target->interact.data.callback,
- "Interact callback pointer cannot be NULL"
- );
- target->interact.data.callback(player, target);
- break;
-
- case ENTITY_INTERACT_NULL:
- break;
-
- default:
- assertUnreachable("Unknown entity interact type");
- break;
- }
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/interact/entityinteract.h b/src/dusk/rpg/entity/interact/entityinteract.h
deleted file mode 100644
index 69f5a4ff..00000000
--- a/src/dusk/rpg/entity/interact/entityinteract.h
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct entity_s entity_t;
-typedef struct cutscene_s cutscene_t;
-
-/**
- * Describes the type of interaction an entity supports.
- */
-typedef enum {
- ENTITY_INTERACT_NULL = 0,
-
- ENTITY_INTERACT_CUTSCENE,
- ENTITY_INTERACT_CUTSCENE_NAME,
- ENTITY_INTERACT_PRINT,
- ENTITY_INTERACT_CALLBACK,
-
- ENTITY_INTERACT_COUNT
-} entityinteracttype_t;
-
-/**
- * Per-type data for an entity's interact component.
- */
-typedef union {
- // A compile-time cutscene_t* (a C-authored, test-only cutscene) - safe
- // to hold onto indefinitely, since it's compiled directly into its own
- // static arrays, never the shared CUTSCENE_SYSTEM.loadedItems buffer (see
- // cutscenesystem.h). Non-const since cutsceneSystemStartCutsceneWith may
- // splice CUTSCENE_ITEM_TYPE_INSERT items into it at runtime.
- cutscene_t *cutscene;
- // A bare cutscene name (as passed to cutsceneCutsceneResolve), resolved
- // lazily at interact time rather than once up front - only one
- // JSON-authored cutscene is ever resident at a time (see
- // CUTSCENE_LOADED_ITEMS_MAX's doc comment), so resolving early and
- // holding onto the result here would go stale the moment any other
- // cutscene loads in the meantime.
- const char_t *cutsceneName;
- const char_t *message;
- void (*callback)(entity_t *player, entity_t *target);
-} entityinteractdata_t;
-
-/**
- * Interact component attached to any entity that can be interacted with.
- * Set type to ENTITY_INTERACT_NULL to mark the entity as non-interactable.
- */
-typedef struct {
- entityinteracttype_t type;
- entityinteractdata_t data;
-} entityinteract_t;
-
-/**
- * Attempts to interact with the target entity on behalf of the player.
- * Dispatches via the interact component; falls back to the entity type
- * callback if no component is set.
- *
- * @param player Pointer to the player entity.
- * @param target Pointer to the entity to interact with.
- */
-void entityInteractWith(entity_t *player, entity_t *target);
diff --git a/src/dusk/rpg/entity/item/CMakeLists.txt b/src/dusk/rpg/entity/item/CMakeLists.txt
deleted file mode 100644
index 6834d19b..00000000
--- a/src/dusk/rpg/entity/item/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- entityitem.c
-)
diff --git a/src/dusk/rpg/entity/item/entityitem.c b/src/dusk/rpg/entity/item/entityitem.c
deleted file mode 100644
index 5d39ce09..00000000
--- a/src/dusk/rpg/entity/item/entityitem.c
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entityitem.h"
-#include "rpg/entity/entity.h"
-#include "assert/assert.h"
-#include "rpg/item/itemgive.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-
-void entityItemInit(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- entity->interact.type = ENTITY_INTERACT_CALLBACK;
- entity->interact.data.callback = entityItemInteract;
-}
-
-void entityItemSet(
- entity_t *entity,
- const itemid_t item,
- const uint8_t quantity
-) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- entity->data.item.item = item;
- entity->data.item.quantity = quantity;
-}
-
-void entityItemInteract(entity_t *player, entity_t *target) {
- assertNotNull(player, "Player entity pointer cannot be NULL");
- assertNotNull(target, "Target entity pointer cannot be NULL");
-
- itemGive(target->data.item.item, target->data.item.quantity);
- target->data.item.collected = true;
-}
-
-void entityItemMovement(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- if(!entity->data.item.collected) return;
- if(uiTextboxMainIsActive()) return;
-
- entitySetChunk(entity, CHUNK_INDEX_INVALID);
- entity->type = ENTITY_TYPE_NULL;
-}
diff --git a/src/dusk/rpg/entity/item/entityitem.h b/src/dusk/rpg/entity/item/entityitem.h
deleted file mode 100644
index 1485f1fa..00000000
--- a/src/dusk/rpg/entity/item/entityitem.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/item/item.h"
-
-typedef struct entity_s entity_t;
-
-typedef struct {
- itemid_t item;
- uint8_t quantity;
- bool_t collected;
-} entityitem_t;
-
-/**
- * Initializes an item entity. Wires up the interact component so that
- * interacting with the entity gives its item and removes it.
- *
- * @param entity Pointer to the entity structure to initialize.
- */
-void entityItemInit(entity_t *entity);
-
-/**
- * Sets the item and quantity represented by an item entity.
- *
- * @param entity Pointer to the entity structure.
- * @param item The item ID to give when picked up.
- * @param quantity The quantity to give when picked up.
- */
-void entityItemSet(
- entity_t *entity,
- const itemid_t item,
- const uint8_t quantity
-);
-
-/**
- * Interact callback for item entities. Gives the entity's item to the
- * player and shows the pickup message. The entity itself is removed once
- * the message has been dismissed, via entityItemMovement.
- *
- * @param player Pointer to the player entity.
- * @param target Pointer to the item entity being picked up.
- */
-void entityItemInteract(entity_t *player, entity_t *target);
-
-/**
- * Update callback for item entities. Removes the entity once its pickup
- * message has been dismissed. No-op if the item hasn't been collected.
- *
- * @param entity Pointer to the entity structure to update.
- */
-void entityItemMovement(entity_t *entity);
diff --git a/src/dusk/rpg/entity/npc/CMakeLists.txt b/src/dusk/rpg/entity/npc/CMakeLists.txt
deleted file mode 100644
index 922a4e1d..00000000
--- a/src/dusk/rpg/entity/npc/CMakeLists.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- npc.c
- npcturn.c
- npcwalk.c
- npcpath.c
-)
diff --git a/src/dusk/rpg/entity/npc/npc.c b/src/dusk/rpg/entity/npc/npc.c
deleted file mode 100644
index 4773e572..00000000
--- a/src/dusk/rpg/entity/npc/npc.c
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg/entity/entity.h"
-#include "assert/assert.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = {
- [NPC_MOVE_TYPE_NULL] = { 0 },
-
- [NPC_MOVE_TYPE_RANDOM_TURN] = {
- npcRandomTurnInit,
- npcRandomTurnMovement
- },
-
- [NPC_MOVE_TYPE_RANDOM_WALK] = {
- npcRandomWalkInit,
- npcRandomWalkMovement
- },
-
- [NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = {
- npcRandomTurnAndWalkInit,
- npcRandomTurnAndWalkMovement
- },
-
- [NPC_MOVE_TYPE_PATH] = {
- npcPathInit,
- npcPathMovement
- },
-};
-
-void npcInit(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
-}
-
-void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- npc_t *npc = &entity->data.npc;
- npc->moveType = moveType;
- if(NPC_MOVE_CALLBACKS[moveType].init != NULL) {
- NPC_MOVE_CALLBACKS[moveType].init(npc);
- }
-}
-
-void npcMovement(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_NPC) return;
-
- npc_t *npc = &entity->data.npc;
- if(npc->interactState != NPC_INTERACT_STATE_NONE) return;
-
- const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType];
- if(cb->movement != NULL) cb->movement(entity);
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/npc/npc.h b/src/dusk/rpg/entity/npc/npc.h
deleted file mode 100644
index be16ee9b..00000000
--- a/src/dusk/rpg/entity/npc/npc.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "npcturn.h"
-#include "npcwalk.h"
-#include "npcpath.h"
-
-typedef struct entity_s entity_t;
-
-typedef enum {
- NPC_INTERACT_STATE_NONE,
- NPC_INTERACT_STATE_CONVERSING,
- NPC_INTERACT_STATE_COUNT
-} npcinteractstate_t;
-
-typedef enum {
- NPC_MOVE_TYPE_NULL,
- NPC_MOVE_TYPE_RANDOM_TURN,
- NPC_MOVE_TYPE_RANDOM_WALK,
- NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK,
- NPC_MOVE_TYPE_PATH,
- NPC_MOVE_TYPE_COUNT
-} npcmovetype_t;
-
-typedef union {
- npcrandomturn_t randomTurn;
- npcrandomwalk_t randomWalk;
- npcrandomturnandwalk_t randomTurnAndWalk;
- npcpath_t path;
-} npcmovedata_t;
-
-typedef struct npc_s {
- npcinteractstate_t interactState;
- npcmovetype_t moveType;
- npcmovedata_t moveData;
-} npc_t;
-
-typedef struct {
- /** Called once when the move type is set. */
- void (*init)(npc_t *npc);
- /** Called each movement tick. */
- void (*movement)(entity_t *entity);
-} npcmovecallback_t;
-
-extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT];
-
-/**
- * Initializes an NPC entity.
- *
- * @param entity Pointer to the entity structure to initialize.
- */
-void npcInit(entity_t *entity);
-
-/**
- * Sets the movement type for an NPC entity.
- *
- * @param entity Pointer to the entity structure.
- * @param moveType The movement type to set.
- */
-void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType);
-
-/**
- * Movement callback for an NPC entity. Gated by cutscene input.
- *
- * @param entity Pointer to the entity structure to update.
- */
-void npcMovement(entity_t *entity);
-
-/**
- * Free movement callback for an NPC entity. Runs always-run move types
- * regardless of cutscene state.
- *
- * @param entity Pointer to the entity structure to update.
- */
-void npcFreeMovement(entity_t *entity);
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c
deleted file mode 100644
index 9d6ea47d..00000000
--- a/src/dusk/rpg/entity/npc/npcpath.c
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "npc.h"
-#include "rpg/entity/entity.h"
-#include "rpg/entity/entitypathstep.h"
-#include "rpg/overworld/worldpos.h"
-#include "assert/assert.h"
-
-void npcPathInit(npc_t *npc) {
- npcpath_t *path = &npc->moveData.path;
- path->count = 0;
- path->index = 0;
-}
-
-void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
- assertNotNull(npc, "NPC must not be NULL");
- assertTrue(
- npc->moveData.path.count < NPC_PATH_COUNT_MAX,
- "NPC path is full"
- );
- npc->moveData.path.positions[npc->moveData.path.count++] = pos;
-}
-
-void npcPathMovement(entity_t *entity) {
- npcpath_t *path = &entity->data.npc.moveData.path;
- if(path->count == 0) return;
- if(!entityCanWalk(entity)) return;
-
- // Advance past any waypoints already reached (including the current one).
- worldpos_t *target = &path->positions[path->index];
- if(entityPathStep(entity, *target, false)) {
- path->index = (path->index + 1) % path->count;
- target = &path->positions[path->index];
- // New target is the same tile - nothing to do this tick
- if(worldPosIsEqual(entity->position, *target)) return;
- entityPathStep(entity, *target, false);
- }
-}
diff --git a/src/dusk/rpg/entity/npc/npcpath.h b/src/dusk/rpg/entity/npc/npcpath.h
deleted file mode 100644
index d6469168..00000000
--- a/src/dusk/rpg/entity/npc/npcpath.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "rpg/overworld/worldpos.h"
-
-typedef struct npc_s npc_t;
-typedef struct entity_s entity_t;
-
-/** Maximum number of waypoints in an NPC path. */
-#define NPC_PATH_COUNT_MAX 8
-
-typedef struct {
- worldpos_t positions[NPC_PATH_COUNT_MAX];
- uint8_t count;
- uint8_t index;
-} npcpath_t;
-
-/**
- * Initializes the path movement data for an NPC.
- *
- * @param npc Pointer to the NPC to initialize.
- */
-void npcPathInit(npc_t *npc);
-
-/**
- * Appends a waypoint to the NPC's path. Has no effect if the path is full.
- *
- * @param npc Pointer to the NPC.
- * @param pos The world position to append.
- */
-void npcPathAddNode(npc_t *npc, const worldpos_t pos);
-
-/**
- * Movement tick for an NPC following a path.
- *
- * @param entity Pointer to the entity to update.
- */
-void npcPathMovement(entity_t *entity);
diff --git a/src/dusk/rpg/entity/npc/npcturn.c b/src/dusk/rpg/entity/npc/npcturn.c
deleted file mode 100644
index 63654ab3..00000000
--- a/src/dusk/rpg/entity/npc/npcturn.c
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "npc.h"
-#include "rpg/entity/entity.h"
-#include "util/random.h"
-#include "time/time.h"
-#include
-
-void npcRandomTurnInit(npc_t *npc) {
- npcrandomturn_t *turn = &npc->moveData.randomTurn;
- turn->frequencyMin = NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT;
- turn->frequencyMax = NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT;
- turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
-}
-
-void npcRandomTurnMovement(entity_t *entity) {
- npcrandomturn_t *turn = &entity->data.npc.moveData.randomTurn;
- turn->timer -= TIME.delta;
- if(turn->timer > 0.0f) return;
- turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
- if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
-}
diff --git a/src/dusk/rpg/entity/npc/npcturn.h b/src/dusk/rpg/entity/npc/npcturn.h
deleted file mode 100644
index 3568bbdb..00000000
--- a/src/dusk/rpg/entity/npc/npcturn.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct npc_s npc_t;
-typedef struct entity_s entity_t;
-
-/** Default min/max seconds between NPC random-turn ticks. */
-#define NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT 2.0f
-#define NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT 4.0f
-
-typedef struct {
- float_t frequencyMin;
- float_t frequencyMax;
- float_t timer;
-} npcrandomturn_t;
-
-/**
- * Initializes the random-turn movement data for an NPC.
- *
- * @param npc Pointer to the NPC to initialize.
- */
-void npcRandomTurnInit(npc_t *npc);
-
-/**
- * Movement tick for an NPC using random-turn movement.
- *
- * @param entity Pointer to the entity to update.
- */
-void npcRandomTurnMovement(entity_t *entity);
diff --git a/src/dusk/rpg/entity/npc/npcwalk.c b/src/dusk/rpg/entity/npc/npcwalk.c
deleted file mode 100644
index dc8794ec..00000000
--- a/src/dusk/rpg/entity/npc/npcwalk.c
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "npc.h"
-#include "rpg/entity/entity.h"
-#include "util/random.h"
-#include "time/time.h"
-#include
-
-void npcRandomWalkInit(npc_t *npc) {
- npcrandomwalk_t *walk = &npc->moveData.randomWalk;
- walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
- walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
- walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
-}
-
-void npcRandomWalkMovement(entity_t *entity) {
- npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
- walk->timer -= TIME.delta;
- if(walk->timer > 0.0f) return;
- walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
- if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
-}
-
-void npcRandomTurnAndWalkInit(npc_t *npc) {
- npcRandomTurnInit(npc);
- npcrandomturnandwalk_t *tw = &npc->moveData.randomTurnAndWalk;
- tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
- tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
- tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
-}
-
-void npcRandomTurnAndWalkMovement(entity_t *entity) {
- npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
-
- tw->turn.timer -= TIME.delta;
- if(tw->turn.timer <= 0.0f) {
- tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
- if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
- }
-
- tw->walk.timer -= TIME.delta;
- if(tw->walk.timer <= 0.0f) {
- tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
- if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
- }
-}
-
diff --git a/src/dusk/rpg/entity/npc/npcwalk.h b/src/dusk/rpg/entity/npc/npcwalk.h
deleted file mode 100644
index 4a65c4f8..00000000
--- a/src/dusk/rpg/entity/npc/npcwalk.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-#include "npcturn.h"
-
-/** Default min/max seconds between NPC random-walk ticks. */
-#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
-#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
-
-typedef struct {
- float_t frequencyMin;
- float_t frequencyMax;
- float_t timer;
-} npcrandomwalk_t;
-
-typedef struct {
- npcrandomturn_t turn;
- npcrandomwalk_t walk;
-} npcrandomturnandwalk_t;
-
-/**
- * Initializes the random-walk movement data for an NPC.
- *
- * @param npc Pointer to the NPC to initialize.
- */
-void npcRandomWalkInit(npc_t *npc);
-
-/**
- * Movement tick for an NPC using random-walk movement.
- *
- * @param entity Pointer to the entity to update.
- */
-void npcRandomWalkMovement(entity_t *entity);
-
-/**
- * Initializes the random-turn-and-walk movement data for an NPC.
- *
- * @param npc Pointer to the NPC to initialize.
- */
-void npcRandomTurnAndWalkInit(npc_t *npc);
-
-/**
- * Movement tick for an NPC using random-turn-and-walk movement.
- *
- * @param entity Pointer to the entity to update.
- */
-void npcRandomTurnAndWalkMovement(entity_t *entity);
-
diff --git a/src/dusk/rpg/entity/player.c b/src/dusk/rpg/entity/player.c
deleted file mode 100644
index c3ff2119..00000000
--- a/src/dusk/rpg/entity/player.c
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entity.h"
-#include "assert/assert.h"
-#include "rpg/rpgcamera.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "ui/focus/uifocus.h"
-#include "ui/screen/game/uigamemenu.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-void playerInit(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- entity->globalId = ENTITY_GLOBAL_ID_PLAYER;
-}
-
-bool_t playerCanInteract(entity_t *entity) {
- return entity->animation == ENTITY_ANIM_IDLE;
-}
-
-void playerInput(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
- if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
-
- // Toggle game menu on pause
- if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
- uiGameMenuClose();
- } else if(
- inputPressed(INPUT_ACTION_PAUSE) &&
- entity->animation == ENTITY_ANIM_IDLE
- ) {
- uiGameMenuOpen();
- }
-
- // Can player act?
- if(UI_FOCUS.count > 0) return;
-
- // Determine direction player may be trying to face/go.
- const playerinputdirmap_t *dirMap;
-
- // First pass, we want to prefer a button relative to what the player is
- // already facing, e.g. if they hold DOWN and start moving down, then tap
- // right we want to prefer down over right.
-
- // Determine current dirmap based on facing direction.
- dirMap = PLAYER_INPUT_DIR_MAP;
- do {
- if(dirMap->direction != entity->direction) continue;
- break;
- } while((++dirMap)->action != 0xFF);
-
- // Are we holding that button?
- if(!inputIsDown(dirMap->action)) {
- // No, so we need to check all directions.
- dirMap = PLAYER_INPUT_DIR_MAP;
- do {
- if(!inputIsDown(dirMap->action)) continue;
- break;
- } while((++dirMap)->action != 0xFF);
- }
-
- // Trying to direct?
- if(dirMap->action != 0xFF) {
- // Is the player attempting to turn? That is, is the player idle and has the
- // turn lockout passed?
- if(entityCanTurn(entity)) {
- if(entity->direction != dirMap->direction) {
- entityTurn(entity, dirMap->direction);
- return;
- }
- }
-
- bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
- if(running && entityCanRun(entity)) {
- entityRun(entity, dirMap->direction);
- } else if(entityCanWalk(entity)) {
- entityWalk(entity, dirMap->direction);
- }
- }
-
- // Interaction
- if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
- worldunit_t x, y, z;
- {
- worldunits_t relX, relY;
- entityDirGetRelative(entity->direction, &relX, &relY);
- x = entity->position.x + relX;
- y = entity->position.y + relY;
- z = entity->position.z;
- }
-
- entity_t *target = entityGetAt((worldpos_t){ x, y, z });
- if(target == NULL) return;
- entityInteractWith(entity, target);
- }
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/entity/player.h b/src/dusk/rpg/entity/player.h
deleted file mode 100644
index 13e369d1..00000000
--- a/src/dusk/rpg/entity/player.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "input/input.h"
-#include "entitydir.h"
-
-typedef struct entity_s entity_t;
-
-typedef struct {
- void *nothing;
-} player_t;
-
-typedef struct {
- inputaction_t action;
- entitydir_t direction;
-} playerinputdirmap_t;
-
-static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = {
- { INPUT_ACTION_UP, ENTITY_DIR_NORTH },
- { INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH },
- { INPUT_ACTION_LEFT, ENTITY_DIR_WEST },
- { INPUT_ACTION_RIGHT, ENTITY_DIR_EAST },
-
- { 0xFF, 0xFF }
-};
-
-/**
- * Initializes a player entity.
- *
- * @param entity Pointer to the entity structure to initialize.
- */
-void playerInit(entity_t *entity);
-
-/**
- * Returns true if the player entity is in a state where it can interact.
- *
- * @param entity Pointer to the player entity to check.
- * @returns True if the entity can interact.
- */
-bool_t playerCanInteract(entity_t *entity);
-
-/**
- * Handles movement logic for the player entity.
- *
- * @param entity Pointer to the player entity structure.
- */
-void playerInput(entity_t *entity);
\ No newline at end of file
diff --git a/src/dusk/rpg/item/CMakeLists.txt b/src/dusk/rpg/item/CMakeLists.txt
deleted file mode 100644
index 29e79fb0..00000000
--- a/src/dusk/rpg/item/CMakeLists.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- item.c
- inventory.c
- backpack.c
- itemgive.c
-)
\ No newline at end of file
diff --git a/src/dusk/rpg/item/backpack.c b/src/dusk/rpg/item/backpack.c
deleted file mode 100644
index b668d0dd..00000000
--- a/src/dusk/rpg/item/backpack.c
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "backpack.h"
-#include "assert/assert.h"
-
-backpack_t BACKPACK;
-
-void backpackInit() {
- for(itemtype_t i = ITEM_TYPE_NULL + 1; i < ITEM.typeCount; i++) {
- inventoryInit(
- &BACKPACK.inventories[i],
- BACKPACK.storage[i],
- ITEM_DEF_COUNT_MAX
- );
- }
-}
-
-inventory_t *backpackGetInventory(const itemtype_t type) {
- assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
- assertTrue(type < ITEM.typeCount, "Item type out of range");
- return &BACKPACK.inventories[type];
-}
-
-void backpackAdd(const itemid_t item, const uint8_t quantity) {
- inventoryAdd(backpackGetInventory(itemGetTypeById(item)), item, quantity);
-}
-
-void backpackRemove(const itemid_t item) {
- inventoryRemove(backpackGetInventory(itemGetTypeById(item)), item);
-}
-
-void backpackSet(const itemid_t item, const uint8_t quantity) {
- inventorySet(backpackGetInventory(itemGetTypeById(item)), item, quantity);
-}
-
-uint8_t backpackGetCount(const itemid_t item) {
- return inventoryGetCount(backpackGetInventory(itemGetTypeById(item)), item);
-}
-
-bool_t backpackItemExists(const itemid_t item) {
- return inventoryItemExists(backpackGetInventory(itemGetTypeById(item)), item);
-}
-
-bool_t backpackIsFull(const itemtype_t type) {
- assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
- assertTrue(type < ITEM.typeCount, "Item type out of range");
- return inventoryIsFull(backpackGetInventory(type));
-}
-
-bool_t backpackItemFull(const itemid_t item) {
- return inventoryItemFull(backpackGetInventory(itemGetTypeById(item)), item);
-}
-
-void backpackSort(
- const itemtype_t type,
- const inventorysort_t sortBy,
- const bool_t reverse
-) {
- assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
- assertTrue(type < ITEM.typeCount, "Item type out of range");
- inventorySort(backpackGetInventory(type), sortBy, reverse);
-}
diff --git a/src/dusk/rpg/item/backpack.h b/src/dusk/rpg/item/backpack.h
deleted file mode 100644
index a8ab4419..00000000
--- a/src/dusk/rpg/item/backpack.h
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "inventory.h"
-
-typedef struct {
- inventorystack_t storage[ITEM_TYPE_DEF_COUNT_MAX][ITEM_DEF_COUNT_MAX];
- inventory_t inventories[ITEM_TYPE_DEF_COUNT_MAX];
-} backpack_t;
-
-extern backpack_t BACKPACK;
-
-/**
- * Initializes the backpack inventory for the player.
- */
-void backpackInit();
-
-/**
- * Returns the inventory for a given item type.
- *
- * @param type The item type.
- * @returns Pointer to the inventory for that type.
- */
-inventory_t *backpackGetInventory(const itemtype_t type);
-
-/**
- * Adds a quantity of an item to the backpack.
- *
- * @param item The item ID to add.
- * @param quantity The quantity to add.
- */
-void backpackAdd(const itemid_t item, const uint8_t quantity);
-
-/**
- * Removes an item from the backpack.
- *
- * @param item The item ID to remove.
- */
-void backpackRemove(const itemid_t item);
-
-/**
- * Sets the quantity of an item in the backpack.
- *
- * @param item The item ID to set.
- * @param quantity The quantity to set.
- */
-void backpackSet(const itemid_t item, const uint8_t quantity);
-
-/**
- * Gets the quantity of an item in the backpack.
- *
- * @param item The item ID to check.
- * @returns The quantity held.
- */
-uint8_t backpackGetCount(const itemid_t item);
-
-/**
- * Checks if an item exists in the backpack (quantity > 0).
- *
- * @param item The item ID to check.
- * @returns true if the item exists.
- */
-bool_t backpackItemExists(const itemid_t item);
-
-/**
- * Checks if the inventory for a given type is full.
- *
- * @param type The item type to check.
- * @returns true if the type's inventory is full.
- */
-bool_t backpackIsFull(const itemtype_t type);
-
-/**
- * Checks if an item's stack is full in the backpack.
- *
- * @param item The item ID to check.
- * @returns true if the item stack is full.
- */
-bool_t backpackItemFull(const itemid_t item);
-
-/**
- * Sorts the inventory for a given item type.
- *
- * @param type The item type whose inventory to sort.
- * @param sortBy The sorting criteria.
- * @param reverse Whether to sort in reverse order.
- */
-void backpackSort(
- const itemtype_t type,
- const inventorysort_t sortBy,
- const bool_t reverse
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/item/inventory.c b/src/dusk/rpg/item/inventory.c
deleted file mode 100644
index 909b7296..00000000
--- a/src/dusk/rpg/item/inventory.c
+++ /dev/null
@@ -1,250 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "inventory.h"
-#include "util/memory.h"
-#include "util/sort.h"
-#include "assert/assert.h"
-
-void inventoryInit(
- inventory_t* inventory,
- inventorystack_t* storage,
- uint8_t storageSize
-) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(storage, "Storage pointer is NULL.");
- assertTrue(storageSize > 0, "Storage size must be greater than zero.");
-
- inventory->storage = storage;
- inventory->storageSize = storageSize;
-
- memoryZero(storage, sizeof(inventorystack_t) * storageSize);
-}
-
-bool_t inventoryItemExists(const inventory_t *inventory, const itemid_t item) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
- assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL.");
-
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
- do {
- if(stack->item == ITEM_ID_NULL) break;
- if(stack->item != item) continue;
- assertTrue(stack->quantity > 0, "Item has quantity zero.");
- return true;
- } while(++stack < end);
-
- return false;
-}
-
-void inventorySet(
- inventory_t *inventory,
- const itemid_t item,
- const uint8_t quantity
-) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
- assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL.");
-
- // If quantity 0, remove.
- if(quantity == 0) return inventoryRemove(inventory, item);
-
- // Search for existing stack.
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
- do {
- // Not in inventory yet, add as new stack.
- if(stack->item == ITEM_ID_NULL) {
- stack->item = item;
- stack->quantity = quantity;
- return;
- }
-
- // Not the stack we're looking for.
- if(stack->item != item) continue;
-
- // Update existing stack.
- stack->quantity = quantity;
- return;
- } while(++stack < end);
-
- // No space in the inventory.
- assertUnreachable("Inventory is full, cannot set more items.");
-}
-
-void inventoryAdd(
- inventory_t *inventory,
- const itemid_t item,
- const uint8_t quantity
-) {
- uint8_t current = inventoryGetCount(inventory, item);
- uint16_t newQuantity = (uint16_t)current + (uint16_t)quantity;
-
- assertTrue(
- newQuantity <= UINT8_MAX,
- "Cannot add item, would overflow maximum quantity."
- );
-
- inventorySet(inventory, item, (uint8_t)newQuantity);
-}
-
-void inventoryRemove(inventory_t *inventory, const itemid_t item) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
- assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL.");
-
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
-
- // Search for existing stack.
- do {
- // End of inventory, item not present.
- if(stack->item == ITEM_ID_NULL) break;
-
- // Not matching stack.
- if(stack->item != item) continue;
-
- // Match found, shift everything else down
- memoryMove(
- stack,
- stack + 1,
- (end - (stack + 1)) * sizeof(inventorystack_t)
- );
-
- // Clear last stack.
- inventorystack_t *last = end - 1;
- last->item = ITEM_ID_NULL;
-
- break;
- } while(++stack < end);
-}
-
-uint8_t inventoryGetCount(const inventory_t *inventory, const itemid_t item) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
- assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL.");
-
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
- do {
- // End of inventory, item not present.
- if(stack->item == ITEM_ID_NULL) break;
-
- // Not matching stack.
- if(stack->item != item) continue;
-
- // Match found, return quantity.
- return stack->quantity;
- } while(++stack < end);
-
- return 0;
-}
-
-bool_t inventoryIsFull(const inventory_t *inventory) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
-
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
- do {
- // Found empty stack, not full.
- if(stack->item == ITEM_ID_NULL) return false;
- } while(++stack < end);
-
- return true;
-}
-
-bool_t inventoryItemFull(const inventory_t *inventory, const itemid_t item) {
- return inventoryGetCount(inventory, item) == ITEM_STACK_QUANTITY_MAX;
-}
-
-// Sorters
-int_t inventorySortById(const void *a, const void *b) {
- const inventorystack_t *stackA = (const inventorystack_t*)a;
- const inventorystack_t *stackB = (const inventorystack_t*)b;
- if(stackA->item < stackB->item) return -1;
- if(stackA->item > stackB->item) return 1;
- return 0;
-}
-
-int_t inventorySortByIdReverse(const void *a, const void *b) {
- const inventorystack_t *stackA = (const inventorystack_t*)a;
- const inventorystack_t *stackB = (const inventorystack_t*)b;
- if(stackA->item < stackB->item) return 1;
- if(stackA->item > stackB->item) return -1;
- return 0;
-}
-
-int_t inventorySortByType(const void *a, const void *b) {
- const inventorystack_t *stackA = (const inventorystack_t*)a;
- const inventorystack_t *stackB = (const inventorystack_t*)b;
- const itemtype_t typeA = itemGetTypeById(stackA->item);
- const itemtype_t typeB = itemGetTypeById(stackB->item);
- if(typeA < typeB) return -1;
- if(typeA > typeB) return 1;
- return 0;
-}
-
-int_t inventorySortByTypeReverse(const void *a, const void *b) {
- const inventorystack_t *stackA = (const inventorystack_t*)a;
- const inventorystack_t *stackB = (const inventorystack_t*)b;
- const itemtype_t typeA = itemGetTypeById(stackA->item);
- const itemtype_t typeB = itemGetTypeById(stackB->item);
- if(typeA < typeB) return 1;
- if(typeA > typeB) return -1;
- return 0;
-}
-
-void inventorySort(
- inventory_t *inventory,
- const inventorysort_t sortBy,
- const bool_t reverse
-) {
- assertNotNull(inventory, "Inventory pointer is NULL.");
- assertNotNull(inventory->storage, "Storage pointer is NULL.");
- assertTrue(inventory->storageSize > 0, "Storage too small.");
- assertTrue(sortBy < INVENTORY_SORT_COUNT, "Invalid sort type.");
-
- // Get count of used stacks
- size_t count = 0;
- inventorystack_t *stack = inventory->storage;
- inventorystack_t *end = stack + inventory->storageSize;
- do {
- if(stack->item == ITEM_ID_NULL) break;
- count++;
- } while(++stack < end);
-
- if(count == 0) return; // Nothing to sort
-
- // Comparator
- sortcompare_t comparator = NULL;
- switch(sortBy) {
- case INVENTORY_SORT_BY_ID: {
- comparator = reverse ? inventorySortByIdReverse : inventorySortById;
- break;
- };
-
- case INVENTORY_SORT_BY_TYPE: {
- comparator = reverse ? inventorySortByTypeReverse : inventorySortByType;
- break;
- };
-
- default:
- assertUnreachable("Invalid sort type.");
- break;
- }
-
- assertNotNull(comparator, "Comparator function is NULL.");
-
- sort((void*)inventory->storage, count, sizeof(inventorystack_t), comparator);
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/item/inventory.h b/src/dusk/rpg/item/inventory.h
deleted file mode 100644
index ba559dfe..00000000
--- a/src/dusk/rpg/item/inventory.h
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/item/item.h"
-
-#define ITEM_STACK_QUANTITY_MAX 99
-
-typedef enum {
- INVENTORY_SORT_BY_ID,
- INVENTORY_SORT_BY_TYPE,
-
- INVENTORY_SORT_COUNT
-} inventorysort_t;
-
-typedef struct {
- itemid_t item;
- uint8_t quantity;
-} inventorystack_t;
-
-typedef struct {
- inventorystack_t *storage;
- uint8_t storageSize;
-} inventory_t;
-
-/**
- * Initializes an inventory.
- *
- * @param inventory The inventory to initialize.
- * @param storage The storage array for the inventory.
- * @param storageSize The size of the storage array.
- */
-void inventoryInit(
- inventory_t* inventory,
- inventorystack_t* storage,
- uint8_t storageSize
-);
-
-/**
- * Checks if a specific item exists in the inventory (and has quantity > 0).
- *
- * @param inventory The inventory to check.
- * @param item The item ID to check.
- * @return true if the item exists, false otherwise.
- */
-bool_t inventoryItemExists(const inventory_t *inventory, const itemid_t item);
-
-/**
- * Sets the quantity of a specific item in the inventory.
- *
- * @param inventory The inventory to modify.
- * @param item The item ID to set.
- * @param quantity The quantity to set.
- */
-void inventorySet(
- inventory_t *inventory,
- const itemid_t item,
- const uint8_t quantity
-);
-
-/**
- * Adds a specific quantity of an item to the inventory.
- *
- * @param inventory The inventory to modify.
- * @param item The item ID to add.
- * @param quantity The quantity to add.
- */
-void inventoryAdd(
- inventory_t *inventory,
- const itemid_t item,
- const uint8_t quantity
-);
-
-/**
- * Removes an item from the inventory.
- *
- * @param inventory The inventory to modify.
- * @param item The item ID to remove.
- */
-void inventoryRemove(inventory_t *inventory, const itemid_t item);
-
-/**
- * Gets the count of a specific item in the inventory.
- *
- * @param inventory The inventory to check.
- * @param item The item ID to check.
- * @return The count of the item in the inventory.
- */
-uint8_t inventoryGetCount(const inventory_t *inventory, const itemid_t item);
-
-/**
- * Checks if the inventory is full.
- *
- * @param inventory The inventory to check.
- * @return true if full, false otherwise.
- */
-bool_t inventoryIsFull(const inventory_t *inventory);
-
-/**
- * Checks if a specific item stack is full in the inventory.
- *
- * @param inventory The inventory to check.
- * @param item The item ID to check.
- * @return true if the item stack is full, false otherwise.
- */
-bool_t inventoryItemFull(const inventory_t *inventory, const itemid_t item);
-
-/**
- * Sorts the inventory based on the specified criteria.
- *
- * @param inventory The inventory to sort.
- * @param sortBy The sorting criteria.
- * @param reverse Whether to sort in reverse order.
- */
-void inventorySort(
- inventory_t *inventory,
- const inventorysort_t sortBy,
- const bool_t reverse
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/item/item.c b/src/dusk/rpg/item/item.c
deleted file mode 100644
index 4be5f980..00000000
--- a/src/dusk/rpg/item/item.c
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "item.h"
-#include "assert/assert.h"
-#include "locale/localemanager.h"
-#include "asset/asset.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "yyjson.h"
-
-item_t ITEM;
-
-itemtype_t itemTypeGetOrCreate(const char_t *id) {
- for(itemtype_t type = ITEM_TYPE_NULL + 1; type < ITEM.typeCount; type++) {
- if(stringEquals(ITEM.types[type].id, id)) return type;
- }
-
- assertTrue(
- ITEM.typeCount < ITEM_TYPE_DEF_COUNT_MAX, "Too many item types defined."
- );
- itemtype_t type = ITEM.typeCount++;
- stringCopy(ITEM.types[type].id, id, ITEM_TYPE_NAME_MAX);
- return type;
-}
-
-errorret_t itemInit(void) {
- memoryZero(&ITEM, sizeof(ITEM));
- ITEM.typeCount = ITEM_TYPE_NULL + 1;
- ITEM.defCount = ITEM_ID_NULL + 1;
-
- assetentry_t *entry =
- assetLock("items/items.jsonc", ASSET_LOADER_TYPE_JSON, NULL);
- errorret_t ret = assetRequireLoaded(entry);
- if(errorIsNotOk(ret)) {
- assetUnlockEntry(entry);
- errorChain(ret);
- }
-
- yyjson_val *root = yyjson_doc_get_root(entry->data.json);
- yyjson_val *items = yyjson_obj_get(root, "items");
-
- yyjson_obj_iter iter = yyjson_obj_iter_with(items);
- yyjson_val *key;
- while((key = yyjson_obj_iter_next(&iter))) {
- assertTrue(ITEM.defCount < ITEM_DEF_COUNT_MAX, "Too many item ids defined.");
-
- yyjson_val *val = yyjson_obj_iter_get_val(key);
- const char_t *typeId = yyjson_get_str(yyjson_obj_get(val, "type"));
-
- itemid_t id = ITEM.defCount++;
- stringCopy(ITEM.defs[id].id, yyjson_get_str(key), ITEM_ID_STRING_MAX);
- ITEM.defs[id].type = itemTypeGetOrCreate(typeId);
- }
-
- assetUnlockEntry(entry);
- errorOk();
-}
-
-const itemdef_t *itemGetDefById(const itemid_t id) {
- assertTrue(id > ITEM_ID_NULL, "Item ID must not be null");
- assertTrue(id < ITEM.defCount, "Item ID out of range");
-
- return &ITEM.defs[id];
-}
-
-itemtype_t itemGetTypeById(const itemid_t id) {
- return itemGetDefById(id)->type;
-}
-
-itemid_t itemGetIdByName(const char_t *name) {
- assertNotNull(name, "name must not be NULL");
-
- for(itemid_t id = ITEM_ID_NULL + 1; id < ITEM.defCount; id++) {
- if(stringEquals(ITEM.defs[id].id, name)) return id;
- }
-
- return ITEM_ID_NULL;
-}
-
-errorret_t itemGetName(
- const itemid_t item,
- char_t *buffer,
- const size_t bufferSize
-) {
- const itemdef_t *def = itemGetDefById(item);
-
- char_t key[ITEM_ID_STRING_MAX + 16];
- stringFormat(key, sizeof(key), "item.%s.name", def->id);
- errorChain(localeManagerGetString(key, buffer, bufferSize, 0));
-
- errorOk();
-}
diff --git a/src/dusk/rpg/item/item.h b/src/dusk/rpg/item/item.h
deleted file mode 100644
index ccb42841..00000000
--- a/src/dusk/rpg/item/item.h
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define ITEM_TYPE_NAME_MAX 12
-#define ITEM_TYPE_DEF_COUNT_MAX 32
-#define ITEM_ID_STRING_MAX 12
-#define ITEM_DEF_COUNT_MAX 64
-
-typedef uint8_t itemtype_t;
-typedef uint16_t itemid_t;
-#define ITEM_TYPE_NULL ((itemtype_t)0)
-#define ITEM_ID_NULL ((itemid_t)0)
-
-typedef struct {
- char_t id[ITEM_TYPE_NAME_MAX];
-} itemtypedef_t;
-
-typedef struct {
- itemtype_t type;
- char_t id[ITEM_ID_STRING_MAX];
-} itemdef_t;
-
-typedef struct {
- itemtypedef_t types[ITEM_TYPE_DEF_COUNT_MAX];
- itemtype_t typeCount;
- itemdef_t defs[ITEM_DEF_COUNT_MAX];
- itemid_t defCount;
-} item_t;
-
-extern item_t ITEM;
-
-/**
- * Initializes the item system.
- *
- * @return Any error that occurs.
- */
-errorret_t itemInit(void);
-
-/**
- * Finds the itemtype_t matching id among the types registered so far,
- * registering a new one if this is the first time it's been seen.
- *
- * @param id The type's identifier string (see itemtypedef_t.id).
- * @return The itemtype_t for id.
- */
-itemtype_t itemTypeGetOrCreate(const char_t *id);
-
-/**
- * Gets the itemdef_t for an item id.
- *
- * @param id The item ID to look up. Must not be ITEM_ID_NULL.
- * @return The itemdef_t for id.
- */
-const itemdef_t *itemGetDefById(const itemid_t id);
-
-/**
- * Gets the itemtype_t for an item id.
- *
- * @param id The item ID to look up. Must not be ITEM_ID_NULL.
- * @return The itemtype_t for id.
- */
-itemtype_t itemGetTypeById(const itemid_t id);
-
-/**
- * Finds the itemid_t whose id string matches name.
- *
- * @param name The item's identifier string (see itemdef_t.id).
- * @return The matching itemid_t, or ITEM_ID_NULL if none matched.
- */
-itemid_t itemGetIdByName(const char_t *name);
-
-/**
- * Gets the localized display name for an item.
- *
- * @param item The item ID to look up. Must not be ITEM_ID_NULL.
- * @param buffer Buffer to write the localized name into.
- * @param bufferSize Size of the buffer.
- * @return Any error that occurs.
- */
-errorret_t itemGetName(
- const itemid_t item,
- char_t *buffer,
- const size_t bufferSize
-);
diff --git a/src/dusk/rpg/item/itemgive.c b/src/dusk/rpg/item/itemgive.c
deleted file mode 100644
index c7416dc2..00000000
--- a/src/dusk/rpg/item/itemgive.c
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "itemgive.h"
-#include "rpg/item/backpack.h"
-#include "rpg/item/item.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "util/string.h"
-#include "error/error.h"
-
-#define ITEM_GIVE_NAME_MAX_CHARS 32
-
-void itemGive(const itemid_t item, const uint8_t quantity) {
- backpackAdd(item, quantity);
-
- char_t name[ITEM_GIVE_NAME_MAX_CHARS];
- errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS));
-
- char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
- stringFormat(
- msg,
- ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
- "Received %s x%u",
- name,
- (uint32_t)quantity
- );
- uiTextboxMainSetText(msg);
-}
diff --git a/src/dusk/rpg/item/itemgive.h b/src/dusk/rpg/item/itemgive.h
deleted file mode 100644
index 5fa02b91..00000000
--- a/src/dusk/rpg/item/itemgive.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/item/item.h"
-
-#define ITEM_GIVE_MESSAGE_MAX_CHARS 256
-
-/**
- * Adds a quantity of an item to the player's backpack and shows a
- * "Received - x" message in the main textbox.
- *
- * @param item The item ID to give.
- * @param quantity The quantity to give.
- */
-void itemGive(const itemid_t item, const uint8_t quantity);
diff --git a/src/dusk/rpg/overworld/CMakeLists.txt b/src/dusk/rpg/overworld/CMakeLists.txt
deleted file mode 100644
index 24c8e3f5..00000000
--- a/src/dusk/rpg/overworld/CMakeLists.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2025 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- chunk.c
- map.c
- maparea.c
- worldpos.c
- tile.c
- tileshape.c
-)
-
-add_subdirectory(global)
-
diff --git a/src/dusk/rpg/overworld/chunk.c b/src/dusk/rpg/overworld/chunk.c
deleted file mode 100644
index 18a5316e..00000000
--- a/src/dusk/rpg/overworld/chunk.c
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "chunk.h"
-
-uint32_t chunkGetTileIndex(const chunkpos_t position) {
- return (position.y * CHUNK_WIDTH) + position.x;
-}
-
-bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
- return (a.x == b.x) && (a.y == b.y) && (a.z == b.z);
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h
deleted file mode 100644
index 48180256..00000000
--- a/src/dusk/rpg/overworld/chunk.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/tile.h"
-#include "worldpos.h"
-
-#define CHUNK_MESH_COUNT_MAX 10
-#define CHUNK_MESH_NAME_MAX 64
-#define CHUNK_ENTITY_COUNT_MAX 10
-#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8
-#define CHUNK_AREA_COUNT_MAX 4
-
-typedef struct assetentry_s assetentry_t;
-
-typedef struct chunk_s {
- chunkpos_t position;
- tile_t tiles[CHUNK_TILE_COUNT];
-
- assetentry_t *dcfEntry;
-
- uint8_t meshCount;
- char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
- vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
- mat4 meshModels[CHUNK_MESH_COUNT_MAX];
- assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
-
- uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
-
- // Map area IDs (into MAP_AREAS) spawned from this chunk's file data.
- // Removed via mapAreaRemove when this chunk unloads, and re-added if it
- // streams back in - unlike entities (tracked by current position via
- // entities[] above), areas have no position-based ownership mechanism of
- // their own, so the owning chunk must track and tear them down directly.
- uint8_t areas[CHUNK_AREA_COUNT_MAX];
-} chunk_t;
-
-/**
- * Gets the tile index for a tile position within a chunk.
- *
- * @param position The position within the chunk.
- * @return The tile index within the chunk.
- */
-uint32_t chunkGetTileIndex(const chunkpos_t position);
-
-/**
- * Checks if two chunk positions are equal.
- *
- * @param a The first chunk position.
- * @param b The second chunk position.
- * @return true if equal, false otherwise.
- */
-bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b);
diff --git a/src/dusk/rpg/overworld/global/CMakeLists.txt b/src/dusk/rpg/overworld/global/CMakeLists.txt
deleted file mode 100644
index 27caaac2..00000000
--- a/src/dusk/rpg/overworld/global/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
-)
diff --git a/src/dusk/rpg/overworld/global/mapareaglobal.h b/src/dusk/rpg/overworld/global/mapareaglobal.h
deleted file mode 100644
index 6f31dcd4..00000000
--- a/src/dusk/rpg/overworld/global/mapareaglobal.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "mapareaglobaldefs.h"
-#include "mapareagloballist.h"
-
-#define MAP_AREA_CALLBACK_LIST_COUNT ( \
- sizeof(MAP_AREA_CALLBACK_LIST) / \
- sizeof(MAP_AREA_CALLBACK_LIST[0]) \
-)
-
-//EOF
diff --git a/src/dusk/rpg/overworld/global/mapareaglobaldefs.h b/src/dusk/rpg/overworld/global/mapareaglobaldefs.h
deleted file mode 100644
index 2f6d49b3..00000000
--- a/src/dusk/rpg/overworld/global/mapareaglobaldefs.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/maparea.h"
-
-#define MAP_AREA_CALLBACK(id) \
- static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger)
-
-#define MAP_AREA_CALLBACK_REF(id) \
- MAP_AREA_CALLBACK_##id
-
-//EOF
diff --git a/src/dusk/rpg/overworld/global/mapareagloballist.h b/src/dusk/rpg/overworld/global/mapareagloballist.h
deleted file mode 100644
index 9768582f..00000000
--- a/src/dusk/rpg/overworld/global/mapareagloballist.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "mapareaglobaldefs.h"
-#include "console/console.h"
-
-MAP_AREA_CALLBACK(1) {
- consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger);
-}
-
-// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal.
-static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = {
- NULL,
- MAP_AREA_CALLBACK_REF(1),
-};
-
-//EOF
diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c
deleted file mode 100644
index 78252261..00000000
--- a/src/dusk/rpg/overworld/map.c
+++ /dev/null
@@ -1,579 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "map.h"
-#include "util/memory.h"
-#include "assert/assert.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-#include "console/console.h"
-#include "util/string.h"
-#include "rpg/entity/global/entityglobal.h"
-#include "rpg/entity/item/entityitem.h"
-#include "rpg/overworld/maparea.h"
-#include "save/save.h"
-#include "time/time.h"
-
-map_t MAP;
-
-void mapChunkLoadingSlotClear(chunk_t *chunk) {
- for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
- if(MAP.loadingChunks[i] != chunk) continue;
- MAP.loadingChunks[i] = NULL;
- return;
- }
-}
-
-errorret_t mapInit() {
- memoryZero(&MAP, sizeof(map_t));
- errorOk();
-}
-
-bool_t mapIsLoaded() {
- return MAP.name[0] != '\0';
-}
-
-errorret_t mapSetMap(const char_t *map) {
- assertNotNull(map, "mapSetMap: map name cannot be NULL");
- assertStrLenMax(map, MAP_NAME_MAX, "Map name too long");
-
- if(mapIsLoaded()) {
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- mapChunkUnload(&MAP.chunks[i]);
- }
- }
-
- memoryZero(&MAP, sizeof(map_t));
- stringCopy(MAP.name, map, MAP_NAME_MAX);
-
- // Keep the current save slot's cached map name in sync so whichever map
- // is active gets persisted next time the slot is actually saved.
- stringCopy(SAVE.slot.cachedData.mapName, MAP.name, MAP_NAME_MAX);
-
- chunkindex_t i = 0;
- for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
- for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
- for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
- chunk_t *chunk = &MAP.chunks[i++];
- chunk->position = (chunkpos_t){
- (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
- };
- errorChain(mapChunkLoad(chunk));
- }
- }
- }
-
- mapRebuildChunkOrder();
- errorOk();
-}
-
-errorret_t mapPositionSet(const chunkpos_t newPos) {
- if(!mapIsLoaded()) errorThrow("No map loaded");
- if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk();
-
- // Separate loaded chunks into "keep" and "free" buckets.
- chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
- uint32_t freedCount = 0;
-
- // Use a boolean grid so the inner load loop can check O(1).
- bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
- memoryZero(posLoaded, sizeof(posLoaded));
-
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- chunk_t *chunk = &MAP.chunks[i];
- chunkunit_t rx = chunk->position.x - newPos.x;
- chunkunit_t ry = chunk->position.y - newPos.y;
- chunkunit_t rz = chunk->position.z - newPos.z;
- if(
- rx >= 0 && rx < MAP_CHUNK_WIDTH &&
- ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
- rz >= 0 && rz < MAP_CHUNK_DEPTH
- ) {
- posLoaded[rx][ry][rz] = true;
- } else {
- mapChunkUnload(chunk);
- chunksFreed[freedCount++] = i;
- }
- }
-
- for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
- for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
- for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
- if(posLoaded[x][y][z]) continue;
- assertTrue(freedCount > 0, "No free chunk slot available.");
- chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
- chunk->position = (chunkpos_t){
- newPos.x + (chunkunit_t)x,
- newPos.y + (chunkunit_t)y,
- newPos.z + (chunkunit_t)z
- };
- errorChain(mapChunkLoad(chunk));
- }
- }
- }
-
- MAP.chunkPosition = newPos;
- mapRebuildChunkOrder();
-
- // chunkOrder was just reshuffled, so entity->chunkIndex (an index into
- // chunkOrder, not a direct chunk_t pointer) may now resolve to a
- // completely different chunk than the one that actually still tracks
- // this entity in its entities[] slot - a chunk that stays loaded keeps
- // its entities[] contents untouched by the reorder, only its slot
- // number within chunkOrder moves. Recompute the cached index directly
- // from each entity's (unmoved) position rather than going through
- // entitySetChunk, which would look up the stale old index, fail to
- // find/clear the entity's real registration, and insert a duplicate
- // entry alongside it. Entities already handled by mapChunkUnload above
- // (detached to CHUNK_INDEX_INVALID, or despawned to ENTITY_TYPE_NULL)
- // are skipped.
- for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
- entity_t *entity = &ENTITIES[i];
- if(entity->type == ENTITY_TYPE_NULL) continue;
- if(entity->chunkIndex == CHUNK_INDEX_INVALID) continue;
-
- chunkpos_t cp;
- worldPosToChunkPos(&entity->position, &cp);
- entity->chunkIndex = mapGetChunkIndexAt(cp);
- }
-
- errorOk();
-}
-
-errorret_t mapUpdate() {
- // TEST ONLY - see MAP_CHUNK_LOAD_DELAY_TEST.
- if(MAP.chunkLoadCooldown > 0.0f) {
- MAP.chunkLoadCooldown -= TIME.delta;
- if(MAP.chunkLoadCooldown <= 0.0f) {
- MAP.chunkLoadCooldown = 0.0f;
- mapChunkLoadNext();
- }
- }
-
- errorOk();
-}
-
-errorret_t mapDispose() {
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- mapChunkUnload(&MAP.chunks[i]);
- }
- MAP.name[0] = '\0';
- errorOk();
-}
-
-void mapChunkUnload(chunk_t *chunk) {
- mapChunkLoadQueueRemove(chunk);
- mapChunkLoadingSlotClear(chunk);
-
- for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
- if(chunk->entities[i] == 0xFF) continue;
- entity_t *entity = &ENTITIES[chunk->entities[i]];
- if(!entityCanUnload(entity)) {
- entitySetChunk(entity, CHUNK_INDEX_INVALID);
- } else {
- entity->type = ENTITY_TYPE_NULL;
- }
- }
-
- memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
-
- for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
- if(chunk->areas[i] == 0xFF) continue;
- mapAreaRemove(chunk->areas[i]);
- }
- memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
-
- if(chunk->dcfEntry != NULL) {
- chunk->dcfEntry->onLoaded = NULL;
- chunk->dcfEntry->onLoadedUser = NULL;
- chunk->dcfEntry->onError = NULL;
- chunk->dcfEntry->onErrorUser = NULL;
- assetUnlockEntry(chunk->dcfEntry);
- chunk->dcfEntry = NULL;
- }
-
- // modelEntries are borrowed pointers, not independently locked - the
- // chunk asset entry (released above) is what actually holds the ref on
- // each model, so nothing to unlock here, just drop our own copies.
- for(uint8_t m = 0; m < chunk->meshCount; m++) {
- chunk->modelEntries[m] = NULL;
- }
- chunk->meshCount = 0;
-}
-
-errorret_t mapChunkLoad(chunk_t *chunk) {
- if(!mapIsLoaded()) errorThrow("No map loaded");
-
- mapChunkLoadQueueRemove(chunk);
- mapChunkLoadingSlotClear(chunk);
-
- if(chunk->dcfEntry != NULL) {
- chunk->dcfEntry->onLoaded = NULL;
- chunk->dcfEntry->onLoadedUser = NULL;
- chunk->dcfEntry->onError = NULL;
- chunk->dcfEntry->onErrorUser = NULL;
- assetUnlockEntry(chunk->dcfEntry);
- chunk->dcfEntry = NULL;
- }
-
- memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
-
- // Normally already empty (mapChunkUnload clears these before a chunk is
- // handed back for reuse), but cleared defensively here too so a reload
- // never leaks a MAP_AREAS slot referenced by a stale owned area ID.
- for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
- if(chunk->areas[i] == 0xFF) continue;
- mapAreaRemove(chunk->areas[i]);
- }
- memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
-
- chunk->meshCount = 0;
-
- char_t name[MAP_FILE_PATH_MAX];
- stringFormat(
- name, sizeof(name),
- "maps/%s/chunks/chunk_%d_%d_%d.json",
- MAP.name,
- (int32_t)chunk->position.x,
- (int32_t)chunk->position.y,
- (int32_t)chunk->position.z
- );
-
- if(!assetFileExists(name)) {
- for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
- // chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
- chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
- }
- errorOk();
- }
-
- assertTrue(
- MAP.loadQueueCount < MAP_CHUNK_COUNT,
- "Chunk load queue overflow"
- );
- MAP.loadQueue[MAP.loadQueueCount++] = chunk;
- mapChunkLoadNext();
- errorOk();
-}
-
-void mapChunkLoadNext() {
- // TEST ONLY - see MAP_CHUNK_LOAD_DELAY_TEST. Skip starting anything new
- // until the cooldown set by the last load we started has expired;
- // mapUpdate() ticks it down and retries this once it hits zero.
- if(MAP.chunkLoadCooldown > 0.0f) return;
-
- for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) {
- if(MAP.loadingChunks[slot] != NULL) continue;
- if(MAP.loadQueueCount == 0) return;
-
- chunk_t *chunk = MAP.loadQueue[0];
- for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
- MAP.loadQueue[i - 1] = MAP.loadQueue[i];
- }
- MAP.loadQueueCount--;
- MAP.loadingChunks[slot] = chunk;
- MAP.chunkLoadCooldown = MAP_CHUNK_LOAD_DELAY_TEST;
-
- char_t name[MAP_FILE_PATH_MAX];
- stringFormat(
- name, sizeof(name),
- "maps/%s/chunks/chunk_%d_%d_%d.json",
- MAP.name,
- (int32_t)chunk->position.x,
- (int32_t)chunk->position.y,
- (int32_t)chunk->position.z
- );
-
- assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
- assertNotNull(entry, "Failed to get chunk asset entry");
- chunk->dcfEntry = entry;
-
- // The entry may already be resident from an earlier load that hasn't
- // been reaped yet - in that case onLoaded/onError already fired once
- // and never will again, so handle the terminal state directly instead
- // of waiting on a subscription that would never trigger. Both of these
- // recurse back into mapChunkLoadNext once they clear this slot, so the
- // outer loop just continues on to try filling the next one.
- if(entry->state == ASSET_ENTRY_STATE_LOADED) {
- mapChunkLoaded(entry, chunk);
- continue;
- }
- if(entry->state == ASSET_ENTRY_STATE_ERROR) {
- mapChunkLoadError(entry, chunk);
- continue;
- }
-
- assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber.");
- assertNull(entry->onError, "Entry already has an onError subscriber.");
- entry->onLoaded = mapChunkLoaded;
- entry->onLoadedUser = chunk;
- entry->onError = mapChunkLoadError;
- entry->onErrorUser = chunk;
- }
-}
-
-void mapChunkLoadQueueRemove(chunk_t *chunk) {
- for(uint32_t i = 0; i < MAP.loadQueueCount; i++) {
- if(MAP.loadQueue[i] != chunk) continue;
- for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) {
- MAP.loadQueue[j - 1] = MAP.loadQueue[j];
- }
- MAP.loadQueueCount--;
- return;
- }
-}
-
-
-chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) {
- if(!mapIsLoaded()) return CHUNK_INDEX_INVALID;
-
- chunkpos_t relPos = {
- position.x - MAP.chunkPosition.x,
- position.y - MAP.chunkPosition.y,
- position.z - MAP.chunkPosition.z
- };
-
- if(
- relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
- relPos.x >= MAP_CHUNK_WIDTH ||
- relPos.y >= MAP_CHUNK_HEIGHT ||
- relPos.z >= MAP_CHUNK_DEPTH
- ) {
- return CHUNK_INDEX_INVALID;
- }
-
- return chunkPosToIndex(&relPos);
-}
-
-chunk_t *mapGetChunk(const chunkindex_t index) {
- if(index == CHUNK_INDEX_INVALID || index >= MAP_CHUNK_COUNT) return NULL;
- if(!mapIsLoaded()) return NULL;
- return MAP.chunkOrder[index];
-}
-
-tile_t mapGetTile(const worldpos_t position) {
- if(!mapIsLoaded()) return TILE_NULL;
-
- chunkpos_t chunkPos;
- worldPosToChunkPos(&position, &chunkPos);
- chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos);
- if(chunkIndex == CHUNK_INDEX_INVALID) return TILE_NULL;
-
- chunk_t *chunk = mapGetChunk(chunkIndex);
- assertNotNull(chunk, "Chunk pointer cannot be NULL");
- chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
- tile_t tile = chunk->tiles[tileIndex];
- if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
- return tile;
-}
-
-bool_t mapGetWalkableZNear(
- const worldunit_t x,
- const worldunit_t y,
- const worldunit_t nearZ,
- worldunit_t *outZ
-) {
- assertNotNull(outZ, "Output Z pointer cannot be NULL");
-
- const worldunit_t candidates[] = {
- nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
- };
- for(uint8_t i = 0; i < 3; i++) {
- const worldpos_t pos = { x, y, candidates[i] };
- if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue;
- *outZ = candidates[i];
- return true;
- }
-
- return false;
-}
-
-entity_t * mapSpawnEntity(
- const entityglobalid_t globalId,
- const worldpos_t position
-) {
- assertTrue(
- globalId > ENTITY_GLOBAL_ID_START,
- "mapSpawnEntity requires a global ID greater than ENTITY_GLOBAL_ID_START"
- );
- assertTrue(
- globalId < ENTITY_GLOBAL_LIST_COUNT,
- "Global ID is out of range for entity global init callbacks"
- );
-
- // Already spawned? Reuse the existing entity instead of making a
- // duplicate - two entities must never share a global ID.
- entity_t *existing = entityGetByGlobalId(globalId);
- if(existing != NULL) return existing;
-
- // See if there is a callback for this entity first.
- const entityglobaldef_t *def = &ENTITY_GLOBAL_LIST[globalId];
- assertNotNull(def, "No global entity definition for this ID");
- assertNotNull(def->callback, "No callback registered for this global ID");
-
- // Get available entity.
- uint8_t index = entityGetAvailable();
- assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity");
-
- // Get the pointer and do the init.
- entity_t *entity = &ENTITIES[index];
- entityInit(entity, def->type);
- entity->globalId = globalId;
- entityPositionSet(entity, position);// Also assigns the entity's chunk.
-
- // Invoke the callback to initialize the entity.
- entityglobalcreate_t create = {
- .entity = entity,
- .position = position
- };
- def->callback(&create);
- return entity;
-}
-
-void mapRebuildChunkOrder() {
- memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder));
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- chunk_t *chunk = &MAP.chunks[i];
- const chunkpos_t rel = {
- chunk->position.x - MAP.chunkPosition.x,
- chunk->position.y - MAP.chunkPosition.y,
- chunk->position.z - MAP.chunkPosition.z
- };
- if(
- rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
- rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
- rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
- ) continue;
- MAP.chunkOrder[chunkPosToIndex(&rel)] = chunk;
- }
-}
-
-void mapChunkLoadError(assetentry_t *entry, void *user) {
- assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL");
- assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
- chunk_t *chunk = (chunk_t *)user;
- if(chunk->dcfEntry != entry) return;
- consolePrint(
- "Chunk load error: %d %d %d",
- (int32_t)chunk->position.x,
- (int32_t)chunk->position.y,
- (int32_t)chunk->position.z
- );
- entry->onLoaded = NULL;
- entry->onLoadedUser = NULL;
- entry->onError = NULL;
- entry->onErrorUser = NULL;
- assetUnlockEntry(chunk->dcfEntry);
- chunk->dcfEntry = NULL;
- memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
-
- mapChunkLoadingSlotClear(chunk);
- mapChunkLoadNext();
-}
-
-void mapChunkLoaded(assetentry_t *entry, void *user) {
- assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL");
- assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
- chunk_t *chunk = (chunk_t *)user;
- if(chunk->dcfEntry != entry) return;
- // consolePrint(
- // "Chunk loaded: %d %d %d",
- // (int32_t)chunk->position.x,
- // (int32_t)chunk->position.y,
- // (int32_t)chunk->position.z
- // );
- uint8_t meshCount = entry->data.chunk.meshCount;
- memoryCopy(
- chunk->tiles,
- entry->data.chunk.tiles,
- sizeof(chunk->tiles)
- );
- worldpos_t wp;
- chunkPosToWorldPos(&chunk->position, &wp);
- vec3 wpf = {
- (float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
- };
- for(uint8_t m = 0; m < meshCount; m++) {
- stringCopy(
- chunk->modelNames[m],
- entry->data.chunk.modelNames[m],
- CHUNK_MESH_NAME_MAX
- );
- glm_vec3_copy(
- entry->data.chunk.meshOffsets[m],
- chunk->meshOffsets[m]
- );
- vec3 scaledOffset = {
- chunk->meshOffsets[m][0],
- chunk->meshOffsets[m][1],
- chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
- };
- vec3 pos;
- glm_vec3_add(wpf, scaledOffset, pos);
- glm_translate_make(chunk->meshModels[m], pos);
- // Borrow the pointer rather than stealing it - the chunk asset entry
- // keeps its own lock on each model (taken once while it loaded) and we
- // keep the chunk asset entry itself locked (see below), so the models
- // stay valid for as long as this chunk_t is using them. The entry may
- // now be reused by a later mapChunkLoad for a different chunk_t once we
- // eventually unlock it in mapChunkUnload, at which point its
- // modelEntries must still be intact for that next reuse to copy from.
- chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
- }
- entry->onLoaded = NULL;
- entry->onLoadedUser = NULL;
- entry->onError = NULL;
- entry->onErrorUser = NULL;
- // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
- // chunk asset entry (and therefore its model locks) alive for as long as
- // this chunk_t is displaying it. Released in mapChunkUnload instead.
- chunk->meshCount = meshCount;
-
- // Spawn entities declared by this chunk's file. Global entities are
- // deduped by mapSpawnEntity itself (a persistent NPC that streams back
- // in won't be duplicated); item entities have no persistent identity, so
- // each reload spawns a fresh one - picking an item up and then leaving
- // and re-entering its chunk will currently respawn it, since nothing
- // tracks "already collected" across a chunk unload/reload yet.
- for(uint8_t s = 0; s < entry->data.chunk.entitySpawnCount; s++) {
- chunkentityspawn_t *spawn = &entry->data.chunk.entitySpawns[s];
- if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL) {
- mapSpawnEntity((entityglobalid_t)spawn->globalId, spawn->position);
- continue;
- }
-
- uint8_t index = entityGetAvailable();
- assertTrue(index != 0xFF, "No available entity slots for chunk spawn");
- entity_t *itemEntity = &ENTITIES[index];
- entityInit(itemEntity, ENTITY_TYPE_ITEM);
- entityItemSet(
- itemEntity, (itemid_t)spawn->itemId, spawn->itemQuantity
- );
- entityPositionSet(itemEntity, spawn->position);
- }
-
- // Spawn map areas declared by this chunk's file, tracked as owned by
- // this chunk so mapChunkUnload can tear them down again.
- for(uint8_t s = 0; s < entry->data.chunk.areaSpawnCount; s++) {
- chunkareaspawn_t *area = &entry->data.chunk.areaSpawns[s];
- uint8_t areaId = mapAreaAddGlobal(
- area->min, area->max, area->callbackId, area->notify, area->trigger
- );
-
- uint8_t slot = 0xFF;
- for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
- if(chunk->areas[i] != 0xFF) continue;
- slot = i;
- break;
- }
- assertTrue(slot != 0xFF, "Chunk has no free owned-area slots");
- chunk->areas[slot] = areaId;
- }
-
- mapChunkLoadingSlotClear(chunk);
- mapChunkLoadNext();
-}
diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h
deleted file mode 100644
index 93b70057..00000000
--- a/src/dusk/rpg/overworld/map.h
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "rpg/overworld/chunk.h"
-#include "rpg/entity/entity.h"
-
-#define MAP_FILE_PATH_MAX 128
-
-// Buffer size for a map's name, including the null terminator - so map
-// names are at most 9 characters.
-#define MAP_NAME_MAX 10
-
-// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/
-// onError) at the same time - everything past this waits in loadQueue.
-#define MAP_CHUNK_LOAD_CONCURRENCY 1
-
-// TEST ONLY - deliberately exaggerated delay (seconds) enforced between
-// starting one chunk load and starting the next, to observe its effect on
-// framerate. See MAP.chunkLoadCooldown / mapChunkLoadNext.
-#define MAP_CHUNK_LOAD_DELAY_TEST 0.05f
-
-typedef struct map_s {
- // Name of the currently loaded map, or an empty string if none is
- // loaded (see mapIsLoaded).
- char_t name[MAP_NAME_MAX];
-
- chunk_t chunks[MAP_CHUNK_COUNT];
- chunk_t *chunkOrder[MAP_CHUNK_COUNT];
- chunkpos_t chunkPosition;
-
- chunk_t *loadQueue[MAP_CHUNK_COUNT];
- uint32_t loadQueueCount;
- chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
-
- // TEST ONLY - seconds remaining before mapChunkLoadNext is allowed to
- // start another chunk load. See MAP_CHUNK_LOAD_DELAY_TEST.
- float_t chunkLoadCooldown;
-} map_t;
-
-extern map_t MAP;
-
-/**
- * Initializes the map subsystem. No map is loaded until mapSetMap is
- * called.
- *
- * @return An error code.
- */
-errorret_t mapInit();
-
-/**
- * Checks if a map is loaded.
- *
- * @return true if a map is loaded, false otherwise.
- */
-bool_t mapIsLoaded();
-
-/**
- * Sets the active map by name, unloading the previously loaded map (if
- * any) first, then starts loading the new map's chunks around chunk
- * position (0, 0, 0).
- *
- * @param map The map's name, at most MAP_NAME_MAX - 1 characters.
- * @return An error code.
- */
-errorret_t mapSetMap(const char_t *map);
-
-/**
- * Updates the map.
- *
- * @return An error code.
- */
-errorret_t mapUpdate();
-
-/**
- * Disposes of the map.
- *
- * @return An error code.
- */
-errorret_t mapDispose();
-
-/**
- * Sets the map position and updates chunks accordingly.
- *
- * @param newPos The new chunk position.
- * @return An error code.
- */
-errorret_t mapPositionSet(const chunkpos_t newPos);
-
-/**
- * Clears chunk's mid-load slot, if it currently holds one.
- *
- * @param chunk The chunk whose mid-load slot to clear.
- */
-void mapChunkLoadingSlotClear(chunk_t *chunk);
-
-/**
- * Unloads a chunk.
- *
- * @param chunk The chunk to unload.
- */
-void mapChunkUnload(chunk_t* chunk);
-
-/**
- * Loads a chunk. Starts async loading without blocking.
- *
- * @param chunk The chunk to load.
- * @return An error code.
- */
-errorret_t mapChunkLoad(chunk_t* chunk);
-
-/**
- * Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are
- * mid-load. Called after mapChunkLoad enqueues a chunk, and again after a
- * mid-load chunk finishes (or is unloaded) to advance the queue.
- */
-void mapChunkLoadNext();
-
-/**
- * Removes a chunk from the load queue if present. Used when a chunk is
- * re-queued or unloaded before its turn to load has come up.
- *
- * @param chunk The chunk to remove from the load queue.
- */
-void mapChunkLoadQueueRemove(chunk_t *chunk);
-
-/**
- * Callback invoked when a chunk DCF asset fails to load. Fills the
- * chunk tiles with TILE_SHAPE_GROUND as a fallback.
- * Always invoked on the main thread.
- *
- * @param entry The failed assetentry_t.
- * @param user The chunk_t that owns the entry.
- */
-void mapChunkLoadError(assetentry_t *entry, void *user);
-
-/**
- * Callback invoked when a chunk DCF asset finishes loading.
- * Always invoked on the main thread.
- *
- * @param entry The loaded assetentry_t.
- * @param user The chunk_t that owns the entry.
- */
-void mapChunkLoaded(assetentry_t *entry, void *user);
-
-/**
- * Rebuilds chunkOrder from the loaded chunks that fall within the
- * current render window. Called whenever chunkPosition changes.
- */
-void mapRebuildChunkOrder();
-
-/**
- * Gets the index of a chunk, within the world, at the given position.
- *
- * @param position The chunk position.
- * @return The index of the chunk, or CHUNK_INDEX_INVALID if out of
- * bounds.
- */
-chunkindex_t mapGetChunkIndexAt(const chunkpos_t position);
-
-/**
- * Gets a chunk by its index.
- *
- * @param chunkIndex The index of the chunk.
- * @return A pointer to the chunk, or NULL if chunkIndex is
- * CHUNK_INDEX_INVALID, out of range, or no map is currently loaded.
- */
-chunk_t * mapGetChunk(const chunkindex_t chunkIndex);
-
-/**
- * Gets the tile at the given world position.
- *
- * @param position The world position.
- * @return The tile at that position, or TILE_NULL if the chunk is unloaded.
- */
-tile_t mapGetTile(const worldpos_t position);
-
-/**
- * Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
- * nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
- * change height by one Z layer between adjacent tiles.
- *
- * @param x The world X coordinate to check.
- * @param y The world Y coordinate to check.
- * @param nearZ The reference Z layer to search outward from.
- * @param outZ Output pointer, set to the resolved Z layer on success.
- * @return true if a walkable tile was found, false otherwise.
- */
-bool_t mapGetWalkableZNear(
- const worldunit_t x,
- const worldunit_t y,
- const worldunit_t nearZ,
- worldunit_t *outZ
-);
-
-/**
- * Spawns a global (persistent) entity into the world at the given position.
- * Asserts globalId is greater than ENTITY_GLOBAL_ID_START - use entityInit
- * directly for ephemeral, non-global entities.
- *
- * @param globalId The global entity ID to assign, must be greater than
- * ENTITY_GLOBAL_ID_START.
- * @param position The world position to spawn the entity at.
- * @return Pointer to the spawned entity.
- */
-entity_t * mapSpawnEntity(
- const entityglobalid_t globalId,
- const worldpos_t position
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/maparea.c b/src/dusk/rpg/overworld/maparea.c
deleted file mode 100644
index 6c1a798b..00000000
--- a/src/dusk/rpg/overworld/maparea.c
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "maparea.h"
-#include "assert/assert.h"
-#include "util/math.h"
-#include "util/memory.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/global/mapareaglobal.h"
-
-maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
-
-void mapAreaInit(
- maparea_t *area,
- const worldpos_t min,
- const worldpos_t max,
- const mapareacallback_t callback,
- const uint8_t notify,
- const uint8_t trigger
-) {
- assertNotNull(area, "Map area pointer cannot be NULL");
- assertNotNull(callback, "Map area callback cannot be NULL");
-
- area->min.x = mathMin(min.x, max.x);
- area->min.y = mathMin(min.y, max.y);
- area->min.z = mathMin(min.z, max.z);
-
- area->max.x = mathMax(min.x, max.x);
- area->max.y = mathMax(min.y, max.y);
- area->max.z = mathMax(min.z, max.z);
-
- area->callback = callback;
- area->notify = notify;
- area->trigger = trigger;
- area->triggerCount = 0;
-
- memorySet(area->entities, 0, sizeof(area->entities));
-}
-
-bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
- assertNotNull(area, "Map area pointer cannot be NULL");
-
- return (
- position.x >= area->min.x && position.x <= area->max.x &&
- position.y >= area->min.y && position.y <= area->max.y &&
- position.z >= area->min.z && position.z <= area->max.z
- );
-}
-
-bool_t mapAreaIsChunkOverlappingOrInside(
- const maparea_t *area,
- const chunk_t *chunk
-) {
- assertNotNull(area, "Map area pointer cannot be NULL");
- assertNotNull(chunk, "Chunk pointer cannot be NULL");
-
- worldpos_t chunkMin, chunkMax;
- chunkPosToWorldPos(&chunk->position, &chunkMin);
- chunkMax.x = chunkMin.x + CHUNK_WIDTH - 1;
- chunkMax.y = chunkMin.y + CHUNK_HEIGHT - 1;
- chunkMax.z = chunkMin.z + CHUNK_DEPTH - 1;
-
- return (
- chunkMin.x <= area->max.x && area->min.x <= chunkMax.x &&
- chunkMin.y <= area->max.y && area->min.y <= chunkMax.y &&
- chunkMin.z <= area->max.z && area->min.z <= chunkMax.z
- );
-}
-
-bool_t mapAreaCanUnload(const maparea_t *area) {
- assertNotNull(area, "Map area pointer cannot be NULL");
-
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
- }
-
- return true;
-}
-
-bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity) {
- assertNotNull(area, "Map area pointer cannot be NULL");
- assertNotNull(entity, "Entity pointer cannot be NULL");
-
- switch(entity->type) {
- case ENTITY_TYPE_PLAYER:
- return (area->notify & MAP_AREA_NOTIFY_PLAYER) != 0;
-
- case ENTITY_TYPE_NPC:
- return (area->notify & MAP_AREA_NOTIFY_NPC) != 0;
-
- default:
- return false;
- }
-}
-
-uint8_t mapAreaAdd(
- const worldpos_t min,
- const worldpos_t max,
- const mapareacallback_t callback,
- const uint8_t notify,
- const uint8_t trigger
-) {
- for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) {
- if(MAP_AREAS[i].callback != NULL) continue;
- mapAreaInit(&MAP_AREAS[i], min, max, callback, notify, trigger);
- return i;
- }
-
- assertUnreachable("No available map area slots");
- return 0xFF;
-}
-
-void mapAreaRemove(const uint8_t id) {
- assertTrue(id < MAP_AREA_COUNT_MAX, "Map area ID is out of range");
- MAP_AREAS[id].callback = NULL;
-}
-
-void mapAreaCheckEntity(entity_t *entity) {
- assertNotNull(entity, "Entity pointer cannot be NULL");
-
- for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) {
- maparea_t *area = &MAP_AREAS[i];
- if(area->callback == NULL) continue;
- if(!mapAreaShouldNotify(area, entity)) continue;
-
- bool_t wasInside = area->entities[entity->id] != 0;
- bool_t isInside = mapAreaIsInside(area, entity->position);
- uint8_t trigger = 0;
-
- if(isInside && !wasInside) {
- area->entities[entity->id] = 1;
- trigger = MAP_TRIGGER_ENTER;
- } else if(isInside && wasInside) {
- trigger = MAP_TRIGGER_STEP;
- } else if(!isInside && wasInside) {
- area->entities[entity->id] = 0;
- trigger = MAP_TRIGGER_EXIT;
- }
-
- if(trigger == 0 || !(area->trigger & trigger)) continue;
- area->triggerCount++;
- area->callback(entity, trigger);
- }
-}
-
-void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
-}
-
-uint8_t mapAreaAddGlobal(
- const worldpos_t min,
- const worldpos_t max,
- const uint16_t callbackId,
- const uint8_t notify,
- const uint8_t trigger
-) {
- assertTrue(callbackId > 0, "Map area callback ID 0 is reserved");
- assertTrue(
- callbackId < MAP_AREA_CALLBACK_LIST_COUNT,
- "Map area callback ID is out of range"
- );
- return mapAreaAdd(
- min, max, MAP_AREA_CALLBACK_LIST[callbackId], notify, trigger
- );
-}
diff --git a/src/dusk/rpg/overworld/maparea.h b/src/dusk/rpg/overworld/maparea.h
deleted file mode 100644
index 8ffb3de9..00000000
--- a/src/dusk/rpg/overworld/maparea.h
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "worldpos.h"
-#include "rpg/entity/entity.h"
-
-typedef struct chunk_s chunk_t;
-typedef struct maparea_s maparea_t;
-
-#define MAP_AREA_COUNT_MAX 32
-
-#define MAP_AREA_NOTIFY_PLAYER (1 << 0)
-#define MAP_AREA_NOTIFY_NPC (1 << 1)
-#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
-
-#define MAP_TRIGGER_STEP (1 << 0)
-#define MAP_TRIGGER_ENTER (1 << 1)
-#define MAP_TRIGGER_EXIT (1 << 2)
-#define MAP_TRIGGER_ALL \
- (MAP_TRIGGER_STEP | MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT)
-
-/**
- * Callback invoked for a map area.
- *
- * @param entity Pointer to the entity associated with the callback.
- * @param trigger Which MAP_TRIGGER_* condition invoked the callback.
- */
-typedef void (*mapareacallback_t)(entity_t *entity, const uint8_t trigger);
-
-typedef struct maparea_s {
- worldpos_t min;
- worldpos_t max;
- mapareacallback_t callback;
- uint8_t notify;
- uint8_t trigger;
- uint8_t entities[ENTITY_COUNT];
-
- // Incremented every time this area's callback is invoked. Lets other
- // systems (e.g. cutscenes) detect "has this area fired since I last
- // checked" without needing to be the callback themselves.
- uint32_t triggerCount;
-} maparea_t;
-
-extern maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
-
-/**
- * Initializes a map area with the given bounds, callback, notify flags
- * and trigger flags.
- *
- * @param area Pointer to the map area to initialize.
- * @param min The minimum world position of the area.
- * @param max The maximum world position of the area.
- * @param callback The callback to invoke for this area. Must not be NULL.
- * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
- * should trigger the callback.
- * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
- * invoke the callback.
- */
-void mapAreaInit(
- maparea_t *area,
- const worldpos_t min,
- const worldpos_t max,
- const mapareacallback_t callback,
- const uint8_t notify,
- const uint8_t trigger
-);
-
-/**
- * Checks whether a world position falls within a map area's bounds
- * (inclusive on all axes).
- *
- * @param area Pointer to the map area to check.
- * @param position The world position to check.
- * @returns true if the position is inside the area.
- */
-bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position);
-
-/**
- * Checks whether a chunk's full footprint overlaps a map area's bounds,
- * rather than just testing the chunk's origin corner.
- *
- * @param area Pointer to the map area to check.
- * @param chunk Pointer to the chunk to check.
- * @returns true if the chunk overlaps the area.
- */
-bool_t mapAreaIsChunkOverlappingOrInside(
- const maparea_t *area,
- const chunk_t *chunk
-);
-
-/**
- * Checks whether a map area is safe to unload, i.e. no entity currently
- * occupies a position within its bounds.
- *
- * @param area Pointer to the map area to check.
- * @returns true if no entity is inside the area.
- */
-bool_t mapAreaCanUnload(const maparea_t *area);
-
-/**
- * Checks whether an entity's type is included in a map area's notify flags.
- *
- * @param area Pointer to the map area to check.
- * @param entity Pointer to the entity to check.
- * @returns true if the entity's type should trigger this area's callback.
- */
-bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity);
-
-/**
- * Adds a map area to the global MAP_AREAS list, in the first free slot.
- * A slot is considered free if its callback is NULL.
- *
- * @param min The minimum world position of the area.
- * @param max The maximum world position of the area.
- * @param callback The callback to invoke for this area. Must not be NULL.
- * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
- * should trigger the callback.
- * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
- * invoke the callback.
- * @returns The ID of the newly added map area.
- */
-uint8_t mapAreaAdd(
- const worldpos_t min,
- const worldpos_t max,
- const mapareacallback_t callback,
- const uint8_t notify,
- const uint8_t trigger
-);
-
-/**
- * Removes a map area from the global MAP_AREAS list, freeing its slot by
- * clearing its callback to NULL.
- *
- * @param id The ID of the map area to remove.
- */
-void mapAreaRemove(const uint8_t id);
-
-/**
- * Checks every active map area against an entity's current position,
- * for areas whose notify flags include the entity's type. Invokes the
- * area's callback with MAP_TRIGGER_ENTER the frame the entity first
- * becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains
- * inside, and MAP_TRIGGER_EXIT the frame it leaves.
- *
- * @param entity Pointer to the entity to check.
- */
-void mapAreaCheckEntity(entity_t *entity);
-
-/**
- * A map area callback that does nothing. Useful for areas whose only
- * purpose is to be waited on (see CUTSCENE_MAP_AREA_WAIT), since the
- * wait is driven by triggerCount rather than callback logic.
- *
- * @param entity Pointer to the entity associated with the callback.
- * @param trigger Which MAP_TRIGGER_* condition invoked the callback.
- */
-void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
-
-/**
- * Adds a map area using a compiled-in callback referenced by ID (see
- * MAP_AREA_CALLBACK_LIST in rpg/overworld/global/mapareagloballist.h),
- * rather than a direct function pointer. This is what lets chunk file
- * data - which can only reference compiled code by a small integer ID,
- * not a function pointer - declare map areas.
- *
- * @param min The minimum world position of the area.
- * @param max The maximum world position of the area.
- * @param callbackId Index into MAP_AREA_CALLBACK_LIST. Must be greater
- * than 0 (0 is reserved) and within range.
- * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
- * should trigger the callback.
- * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
- * invoke the callback.
- * @returns The ID of the newly added map area.
- */
-uint8_t mapAreaAddGlobal(
- const worldpos_t min,
- const worldpos_t max,
- const uint16_t callbackId,
- const uint8_t notify,
- const uint8_t trigger
-);
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/tile.c b/src/dusk/rpg/overworld/tile.c
deleted file mode 100644
index 34e25cda..00000000
--- a/src/dusk/rpg/overworld/tile.c
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "tile.h"
diff --git a/src/dusk/rpg/overworld/tile.h b/src/dusk/rpg/overworld/tile.h
deleted file mode 100644
index 82f30850..00000000
--- a/src/dusk/rpg/overworld/tile.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "tileshape.h"
-
-typedef struct {
- tileshape_t shape;
-
- // Local Z layer (0..CHUNK_DEPTH-1) this tile occupies within its chunk's
- // depth slab. Used for entity navigation since chunk_t only stores one
- // tile per X/Y column rather than one per X/Y/Z.
- uint8_t z;
-} tile_t;
-
-#define TILE_NULL ((tile_t){ .shape = TILE_SHAPE_NULL, .z = 0 })
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/tileshape.c b/src/dusk/rpg/overworld/tileshape.c
deleted file mode 100644
index 13124c6f..00000000
--- a/src/dusk/rpg/overworld/tileshape.c
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "tileshape.h"
-
-bool_t tileShapeIsWalkable(const tileshape_t shape) {
- switch(shape) {
- case TILE_SHAPE_NULL:
- return false;
-
- default:
- return true;
- }
-}
-
-bool_t tileShapeIsRamp(const tileshape_t shape) {
- switch(shape) {
- case TILE_SHAPE_RAMP_NORTH:
- case TILE_SHAPE_RAMP_SOUTH:
- case TILE_SHAPE_RAMP_EAST:
- case TILE_SHAPE_RAMP_WEST:
- case TILE_SHAPE_RAMP_NORTHEAST:
- case TILE_SHAPE_RAMP_NORTHWEST:
- case TILE_SHAPE_RAMP_SOUTHEAST:
- case TILE_SHAPE_RAMP_SOUTHWEST:
- case TILE_SHAPE_RAMP_NORTHEAST_INNER:
- case TILE_SHAPE_RAMP_NORTHWEST_INNER:
- case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
- case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
- return true;
-
- default:
- return false;
- }
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/tileshape.h b/src/dusk/rpg/overworld/tileshape.h
deleted file mode 100644
index 1187ac70..00000000
--- a/src/dusk/rpg/overworld/tileshape.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/entity/entitydir.h"
-
-typedef enum {
- TILE_SHAPE_NULL = 0,
-
- TILE_SHAPE_GROUND = 1,
- TILE_SHAPE_RAMP_NORTH = 2,
- TILE_SHAPE_RAMP_EAST = 3,
- TILE_SHAPE_RAMP_SOUTH = 4,
- TILE_SHAPE_RAMP_WEST = 5,
- TILE_SHAPE_RAMP_NORTHEAST = 6,
- TILE_SHAPE_RAMP_NORTHWEST = 7,
- TILE_SHAPE_RAMP_SOUTHEAST = 8,
- TILE_SHAPE_RAMP_SOUTHWEST = 9,
- TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
- TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
- TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
- TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
-
- TILE_SHAPE_COUNT
-} tileshape_t;
-
-/**
- * Returns whether or not the given tile shape is a ramp.
- *
- * @param shape The tile shape to check.
- * @return bool_t True if ramp, false if not.
- */
-bool_t tileShapeIsRamp(const tileshape_t shape);
-
-/**
- * Returns whether or not the given tile shape is walkable.
- *
- * @param shape The tile shape to check.
- * @return bool_t True if walkable, false if not.
- */
-bool_t tileShapeIsWalkable(const tileshape_t shape);
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/worldpos.c b/src/dusk/rpg/overworld/worldpos.c
deleted file mode 100644
index 6112bf9a..00000000
--- a/src/dusk/rpg/overworld/worldpos.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "worldpos.h"
-#include "assert/assert.h"
-#include "util/endian.h"
-#include "util/memory.h"
-
-bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b) {
- return a.x == b.x && a.y == b.y && a.z == b.z;
-}
-
-void chunkPosToWorldPos(const chunkpos_t* chunkPos, worldpos_t* out) {
- assertNotNull(chunkPos, "Chunk position pointer cannot be NULL");
- assertNotNull(out, "Output world position pointer cannot be NULL");
-
- out->x = (worldunit_t)(chunkPos->x * CHUNK_WIDTH);
- out->y = (worldunit_t)(chunkPos->y * CHUNK_HEIGHT);
- out->z = (worldunit_t)(chunkPos->z * CHUNK_DEPTH);
-}
-
-void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out) {
- assertNotNull(worldPos, "World position pointer cannot be NULL");
- assertNotNull(out, "Output chunk position pointer cannot be NULL");
-
- if(worldPos->x < 0) {
- out->x = (chunkunit_t)((worldPos->x - (CHUNK_WIDTH - 1)) / CHUNK_WIDTH);
- } else {
- out->x = (chunkunit_t)(worldPos->x / CHUNK_WIDTH);
- }
-
- if(worldPos->y < 0) {
- out->y = (chunkunit_t)((worldPos->y - (CHUNK_HEIGHT - 1)) / CHUNK_HEIGHT);
- } else {
- out->y = (chunkunit_t)(worldPos->y / CHUNK_HEIGHT);
- }
-
- if(worldPos->z < 0) {
- out->z = (chunkunit_t)((worldPos->z - (CHUNK_DEPTH - 1)) / CHUNK_DEPTH);
- } else {
- out->z = (chunkunit_t)(worldPos->z / CHUNK_DEPTH);
- }
-}
-
-chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
- assertNotNull(worldPos, "World position pointer cannot be NULL");
-
- uint8_t localX, localY;
- if(worldPos->x < 0) {
- localX = (uint8_t)(
- (CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH)
- );
- } else {
- localX = (uint8_t)(worldPos->x % CHUNK_WIDTH);
- }
-
- if(worldPos->y < 0) {
- localY = (uint8_t)(
- (CHUNK_HEIGHT - 1) - ((-worldPos->y - 1) % CHUNK_HEIGHT)
- );
- } else {
- localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT);
- }
-
- chunktileindex_t chunkTileIndex = (chunktileindex_t)(
- (localY * CHUNK_WIDTH) + localX
- );
- assertTrue(
- chunkTileIndex < CHUNK_TILE_COUNT,
- "Calculated chunk tile index is out of bounds"
- );
- return chunkTileIndex;
-}
-
-uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos) {
- assertNotNull(worldPos, "World position pointer cannot be NULL");
-
- if(worldPos->z < 0) {
- return (uint8_t)(
- (CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH)
- );
- }
- return (uint8_t)(worldPos->z % CHUNK_DEPTH);
-}
-
-chunkindex_t chunkPosToIndex(const chunkpos_t* pos) {
- assertNotNull(pos, "Chunk position pointer cannot be NULL");
-
- chunkindex_t chunkIndex = (chunkindex_t)(
- (pos->z * MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT) +
- (pos->y * MAP_CHUNK_WIDTH) +
- pos->x
- );
-
- return chunkIndex;
-}
-
-worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset) {
- assertNotNull(data, "Data pointer cannot be NULL");
- assertNotNull(offset, "Offset pointer cannot be NULL");
-
- int16_t value;
- memoryCopy(&value, data + *offset, sizeof(int16_t));
- *offset += sizeof(int16_t);
- return (worldunit_t)endianLittleToHost16((uint16_t)value);
-}
-
-worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset) {
- assertNotNull(data, "Data pointer cannot be NULL");
- assertNotNull(offset, "Offset pointer cannot be NULL");
-
- worldpos_t pos;
- pos.x = worldUnitReadLE(data, offset);
- pos.y = worldUnitReadLE(data, offset);
- pos.z = worldUnitReadLE(data, offset);
- return pos;
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h
deleted file mode 100644
index 3169839c..00000000
--- a/src/dusk/rpg/overworld/worldpos.h
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "display/display.h"
-#include "util/math.h"
-
-#define TILE_SIZE_PIXELS 24
-
-// World-space Z distance of one Z-layer/story, in render/world-float
-// space. A ramp rises this much over a horizontal run of 1 tile-width;
-// chosen as 1/sqrt(2) so the ramp's slope, derived from a 45-45-90
-// triangle with hypotenuse 1, matches the length of a flat tile edge.
-#define WORLD_LAYER_HEIGHT 0.70710678f
-
-#define CHUNK_WIDTH 16
-#define CHUNK_HEIGHT 16
-#define CHUNK_DEPTH 8
-
-// Chunks store one tile per X/Y column, not one per X/Y/Z - each tile
-// records its own local Z layer (see tile_t) instead.
-#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT)
-
-#define MAP_CHUNK_WIDTH 3
-#define MAP_CHUNK_HEIGHT 3
-#define MAP_CHUNK_DEPTH 4
-#define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH)
-
-#define ENTITY_COUNT 32
-
-typedef int16_t worldunit_t;
-typedef int16_t chunkunit_t;
-typedef int16_t chunkindex_t;
-typedef uint32_t chunktileindex_t;
-
-// Sentinel chunkindex_t value meaning "no chunk" - returned by
-// mapGetChunkIndexAt when a position falls outside every loaded chunk,
-// and used throughout as the "not assigned to any chunk" state.
-#define CHUNK_INDEX_INVALID ((chunkindex_t)-1)
-
-typedef int32_t worldunits_t;
-typedef int32_t chunkunits_t;
-
-typedef struct worldpos2d_s {
- worldunit_t x, y;
-} worldpos2d_t;
-
-typedef struct worldpos_s {
- worldunit_t x, y, z;
-} worldpos_t;
-
-typedef struct chunkpos_t {
- chunkunit_t x, y, z;
-} chunkpos_t;
-
-/**
- * Compares two world positions for equality.
- *
- * @param a The first world position.
- * @param b The second world position.
- * @return true if equal, false otherwise.
- */
-bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b);
-
-/**
- * Converts a world position to a chunk position.
- *
- * @param worldPos The world position.
- * @param out The output chunk position.
- */
-void chunkPosToWorldPos(const chunkpos_t* chunkPos, worldpos_t* out);
-
-/**
- * Converts a chunk position to a world position.
- *
- * @param worldPos The world position.
- * @param out The output chunk position.
- */
-void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out);
-
-/**
- * Converts a position in world-space to an index inside a chunk that the tile
- * resides in.
- *
- * @param worldPos The world position.
- * @return The tile index within the chunk.
- */
-chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos);
-
-/**
- * Converts a world-space Z coordinate to the local Z layer (0..CHUNK_DEPTH-1)
- * within the chunk that owns it, for comparison against tile_t.z.
- *
- * @param worldPos The world position.
- * @return The local Z layer within the owning chunk.
- */
-uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos);
-
-/**
- * Converts a chunk position to a world position.
- *
- * @param worldPos The world position.
- * @param out The output chunk position.
- */
-chunkindex_t chunkPosToIndex(const chunkpos_t* pos);
-
-/**
- * Reads a little-endian worldunit_t from a potentially-unaligned offset
- * into a binary asset buffer, advancing *offset past it. Shared by the
- * chunk and cutscene asset loaders' binary formats, both of which encode
- * world positions the same way.
- *
- * @param data The buffer to read from.
- * @param offset In/out cursor into data, advanced past the value read.
- * @return The decoded worldunit_t.
- */
-worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset);
-
-/**
- * Reads a little-endian worldpos_t (x, then y, then z) from a
- * potentially-unaligned offset into a binary asset buffer, advancing
- * *offset past it.
- *
- * @param data The buffer to read from.
- * @param offset In/out cursor into data, advanced past the value read.
- * @return The decoded worldpos_t.
- */
-worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset);
\ No newline at end of file
diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c
deleted file mode 100644
index 291f35cf..00000000
--- a/src/dusk/rpg/rpg.c
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpg.h"
-#include "entity/entity.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/maparea.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/item/item.h"
-#include "rpg/item/backpack.h"
-#include "rpg/battle/party.h"
-#include "rpg/battle/move/battlemove.h"
-#include "save/save.h"
-#include "ui/rpg/textbox/uitextboxminilist.h"
-#include "time/time.h"
-#include "rpgcamera.h"
-#include "util/memory.h"
-#include "assert/assert.h"
-#include "error/error.h"
-#include "scene/scene.h"
-#include "ui/rpg/uiemoji.h"
-#include "rpg/story/story.h"
-
-errorret_t rpgInit(void) {
- memoryZero(ENTITIES, sizeof(ENTITIES));
- memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
-
- errorChain(storyInit());
- errorChain(itemInit());
- errorChain(battleMoveInit());
- backpackInit();
- cutsceneSystemInit();
- errorChain(mapInit());
- rpgCameraInit();
-
- // The player is the one entity that isn't sourced from map/chunk data -
- // every other entity (NPCs, items) and map area comes from the loaded
- // chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
- uint8_t entIndex = entityGetAvailable();
- assertTrue(entIndex != 0xFF, "No available entity slots!.");
- entity_t *ent = &ENTITIES[entIndex];
- entityInit(ent, ENTITY_TYPE_PLAYER);
- entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk.
- RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
- RPG_CAMERA.followEntity.followEntityId = ent->id;
-
- // Starting inventory.
- backpackAdd(itemGetIdByName("potion"), 5);
- backpackAdd(itemGetIdByName("potato"), 3);
- backpackAdd(itemGetIdByName("apple"), 8);
-
-
- // All Good!
- errorOk();
-}
-
-errorret_t rpgUpdate(void) {
- #ifdef DUSK_TIME_DYNAMIC
- if(TIME.dynamicUpdate) {
- errorOk();
- }
- #endif
-
- // TODO: Do not update if the scene is not the map scene?
- errorChain(mapUpdate());
-
- // Update overworld ents - only while actually in the overworld. Entities
- // (the player among them) keep existing across scene changes, but their
- // input/movement/animation logic doesn't make sense to run mid-battle or
- // before the initial scene has handed off to the overworld.
- if(SCENE.current == SCENE_TYPE_OVERWORLD) {
- entity_t *ent = &ENTITIES[0];
- do {
- if(ent->type == ENTITY_TYPE_NULL) continue;
- entityUpdate(ent);
- } while(++ent < &ENTITIES[ENTITY_COUNT]);
- }
-
- cutsceneSystemUpdate();
- errorChain(rpgCameraUpdate());
- errorOk();
-}
-
-errorret_t rpgDispose(void) {
- cutsceneSystemDispose();
- errorChain(mapDispose());
-
- errorOk();
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/rpg.h b/src/dusk/rpg/rpg.h
deleted file mode 100644
index 9396c326..00000000
--- a/src/dusk/rpg/rpg.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct {
- int32_t nothing;
-} rpg_t;
-
-/**
- * Initialize the RPG subsystem.
- *
- * @return An error code and state.
- */
-errorret_t rpgInit(void);
-
-/**
- * Update the RPG subsystem.
- *
- * @return An error code.
- */
-errorret_t rpgUpdate(void);
-
-/**
- * Dispose of the RPG subsystem.
- *
- * @return An error code.
- */
-errorret_t rpgDispose(void);
\ No newline at end of file
diff --git a/src/dusk/rpg/rpgcamera.c b/src/dusk/rpg/rpgcamera.c
deleted file mode 100644
index d3742d88..00000000
--- a/src/dusk/rpg/rpgcamera.c
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "rpgcamera.h"
-#include "util/memory.h"
-#include "util/random.h"
-#include "rpg/entity/entity.h"
-#include "rpg/overworld/map.h"
-#include "assert/assert.h"
-#include "time/time.h"
-
-#include "display/screen/screen.h"
-
-static const float_t RPG_CAMERA_SHAKE_AMOUNTS[] = {
- 0.0f, 0.5f, 1.0f, 2.0f, 3.0f
-};
-
-rpgcamera_t RPG_CAMERA;
-
-void rpgCameraInit(void) {
- memoryZero(&RPG_CAMERA, sizeof(rpgcamera_t));
- RPG_CAMERA.projectionDirty = true;
-}
-
-void rpgCameraShake(uint8_t amount, float_t duration) {
- assertTrue(amount <= 4, "Camera shake amount must be between 0 and 4");
- RPG_CAMERA.shakeAmount = RPG_CAMERA_SHAKE_AMOUNTS[amount];
- RPG_CAMERA.shakeDuration = duration;
- RPG_CAMERA.shakeTime = 0.0f;
-}
-
-void rpgCameraGetPosition(vec3 out) {
- switch(RPG_CAMERA.mode) {
- case RPG_CAMERA_MODE_FREE:
- glm_vec3_copy(RPG_CAMERA.free, out);
- return;
-
- case RPG_CAMERA_MODE_FOLLOW_ENTITY: {
- entity_t *entity = &ENTITIES[RPG_CAMERA.followEntity.followEntityId];
- if(entity->type == ENTITY_TYPE_NULL) {
- glm_vec3_zero(out);
- return;
- }
- glm_vec3_copy(entity->renderPosition, out);
- return;
- }
-
- default:
- assertUnreachable("Invalid RPG camera mode");
- }
-}
-
-void rpgCameraUpdateProjection(void) {
- #ifdef DUSK_DISPLAY_WIDTH
- if(!RPG_CAMERA.projectionDirty) return;
- RPG_CAMERA.projectionDirty = false;
- #endif
-
- glm_perspective(
- glm_rad(RPG_CAMERA_FOV),
- SCREEN.aspect,
- 0.1f,
- 100.0f,
- RPG_CAMERA.projection
- );
-}
-
-void rpgCameraUpdateEye(void) {
- float_t fov = glm_rad(RPG_CAMERA_FOV);
- float_t pixelsPerUnit = TILE_SIZE_PIXELS;
- float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
- float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
- float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
-
- vec3 target;
- rpgCameraGetPosition(target);
- glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target);
-
- if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
- float_t t = 1.0f - (RPG_CAMERA.shakeTime / RPG_CAMERA.shakeDuration);
- float_t magnitude = RPG_CAMERA.shakeAmount * t;
- target[0] += randomFloat(-magnitude, magnitude);
- target[2] += randomFloat(-magnitude, magnitude);
- }
-
- glm_lookat(
- (vec3){ target[0], target[1] + offset, target[2] + z },
- target,
- (vec3){ 0, 1, 0 }, // up
- RPG_CAMERA.eye
- );
-}
-
-void rpgCameraToScreen(vec3 worldPos, vec2 out) {
- mat4 viewProj;
- glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj);
-
- vec4 viewport = {
- 0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
- };
- vec3 window;
- glm_project(worldPos, viewProj, viewport, window);
-
- out[0] = window[0];
- out[1] = (float_t)SCREEN.height - window[1];
-}
-
-errorret_t rpgCameraUpdate(void) {
- if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
- RPG_CAMERA.shakeTime += TIME.delta;
- }
-
- if(!mapIsLoaded()) errorOk();
-
- vec3 pos;
- rpgCameraGetPosition(pos);
-
- chunkpos_t chunkPos = {
- .x = (chunkunit_t)floorf(pos[0] / CHUNK_WIDTH),
- .y = (chunkunit_t)floorf(pos[1] / CHUNK_HEIGHT),
- .z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH)
- };
-
- errorChain(mapPositionSet((chunkpos_t){
- .x = chunkPos.x - (MAP_CHUNK_WIDTH / 2),
- .y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2),
- .z = chunkPos.z - (MAP_CHUNK_DEPTH / 2)
- }));
- errorOk();
-}
\ No newline at end of file
diff --git a/src/dusk/rpg/rpgcamera.h b/src/dusk/rpg/rpgcamera.h
deleted file mode 100644
index 12742ff7..00000000
--- a/src/dusk/rpg/rpgcamera.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Copyright (c) 2025 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/overworld/worldpos.h"
-#include "error/error.h"
-
-#define RPG_CAMERA_FOV 35.0f
-
-typedef enum {
- RPG_CAMERA_MODE_FREE,
- RPG_CAMERA_MODE_FOLLOW_ENTITY,
-} rpgcameramode_t;
-
-typedef struct {
- rpgcameramode_t mode;
-
- union {
- vec3 free;
-
- struct {
- uint8_t followEntityId;
- } followEntity;
- };
-
- mat4 eye;
- mat4 projection;
- bool_t projectionDirty;
-
- float_t shakeAmount;
- float_t shakeDuration;
- float_t shakeTime;
-} rpgcamera_t;
-
-extern rpgcamera_t RPG_CAMERA;
-
-/**
- * Initializes the RPG camera.
- */
-void rpgCameraInit(void);
-
-/**
- * Gets the RPG camera's position.
- *
- * @param out Output vec3 filled with the camera world position.
- */
-void rpgCameraGetPosition(vec3 out);
-
-/**
- * Updates the RPG camera.
- *
- * @return An error code.
- */
-errorret_t rpgCameraUpdate(void);
-
-/**
- * Recomputes the camera projection matrix and stores it in
- * RPG_CAMERA.projection. On platforms with a fixed display size the
- * matrix is computed once and cached; on dynamic-display platforms it
- * is recomputed every call.
- */
-void rpgCameraUpdateProjection(void);
-
-/**
- * Recomputes the camera eye/view matrix from the camera's current mode
- * and position, and stores it in RPG_CAMERA.eye. Unlike the projection
- * matrix this is never cached, since the camera position can change
- * every frame.
- */
-void rpgCameraUpdateEye(void);
-
-/**
- * Shakes the RPG camera, randomly offsetting its position by a
- * decreasing amount over the given duration.
- *
- * @param amount Shake strength from 0 (no shake) to 4 (three tiles).
- * 1 is half a tile, 2 is a full tile, 3 is two tiles, and 4 is three
- * tiles.
- * @param duration How long the shake lasts, in seconds.
- */
-void rpgCameraShake(uint8_t amount, float_t duration);
-
-/**
- * Converts a world-space position to screen-space pixel coordinates,
- * using the camera's current eye and projection matrices.
- *
- * @param worldPos The world-space position to convert.
- * @param out Output vec2 filled with the screen-space pixel position.
- */
-void rpgCameraToScreen(vec3 worldPos, vec2 out);
\ No newline at end of file
diff --git a/src/dusk/rpg/story/CMakeLists.txt b/src/dusk/rpg/story/CMakeLists.txt
deleted file mode 100644
index 8a945222..00000000
--- a/src/dusk/rpg/story/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- story.c
-)
diff --git a/src/dusk/rpg/story/story.c b/src/dusk/rpg/story/story.c
deleted file mode 100644
index 007b1ff7..00000000
--- a/src/dusk/rpg/story/story.c
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "story.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-#include "assert/assert.h"
-#include "save/save.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "yyjson.h"
-
-storyflagmap_t STORY_FLAG_MAP[STORY_FLAG_COUNT_MAX];
-storyflag_t STORY_FLAG_COUNT;
-
-errorret_t storyInit(void) {
- memoryZero(STORY_FLAG_MAP, sizeof(STORY_FLAG_MAP));
- STORY_FLAG_COUNT = 0;
-
- assetentry_t *entry = assetLock(
- "story/storyflag.jsonc", ASSET_LOADER_TYPE_JSON, NULL
- );
- errorret_t ret = assetRequireLoaded(entry);
- if(errorIsNotOk(ret)) {
- assetUnlockEntry(entry);
- errorChain(ret);
- }
-
- yyjson_val *root = yyjson_doc_get_root(entry->data.json);
- yyjson_val *flags = yyjson_obj_get(root, "flags");
-
- yyjson_obj_iter iter = yyjson_obj_iter_with(flags);
- yyjson_val *key;
- while((key = yyjson_obj_iter_next(&iter))) {
- assertTrue(
- STORY_FLAG_COUNT < STORY_FLAG_COUNT_MAX, "Too many story flags defined."
- );
-
- yyjson_val *val = yyjson_obj_iter_get_val(key);
- yyjson_val *initial = yyjson_obj_get(val, "initial");
-
- storyflag_t flag = STORY_FLAG_COUNT++;
- stringCopy(STORY_FLAG_MAP[flag].name, yyjson_get_str(key), STORY_FLAG_NAME_MAX);
- STORY_FLAG_MAP[flag].initial = yyjson_is_bool(initial) ?
- (storyflagval_t)yyjson_get_bool(initial) :
- (storyflagval_t)yyjson_get_int(initial);
- }
-
- assetUnlockEntry(entry);
- errorOk();
-}
-
-storyflag_t storyGetFlagByName(const char_t *name) {
- assertNotNull(name, "name must not be NULL");
-
- for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
- if(stringEquals(STORY_FLAG_MAP[flag].name, name)) return flag;
- }
-
- return STORY_FLAG_COUNT_MAX;
-}
-
-const char_t * storyGetNameByFlag(storyflag_t flag) {
- if(flag >= STORY_FLAG_COUNT) return NULL;
- return STORY_FLAG_MAP[flag].name;
-}
-
-storyflagval_t storyGetFlagValue(storyflag_t flag) {
- if(flag >= STORY_FLAG_COUNT) return 0;
- return SAVE.slot.storyFlags[flag];
-}
-
-void storySetFlagValue(storyflag_t flag, storyflagval_t value) {
- assertTrue(flag < STORY_FLAG_COUNT, "Bad story flag");
- SAVE.slot.storyFlags[flag] = value;
- SAVE.slotDirty = true;
-}
-
-void storySetFlagValueByName(const char_t *name, storyflagval_t value) {
- storyflag_t flag = storyGetFlagByName(name);
- assertTrue(flag < STORY_FLAG_COUNT, "Unknown story flag");
- storySetFlagValue(flag, value);
-}
diff --git a/src/dusk/rpg/story/story.h b/src/dusk/rpg/story/story.h
deleted file mode 100644
index 108fe0fe..00000000
--- a/src/dusk/rpg/story/story.h
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define STORY_FLAG_COUNT_MAX 256
-#define STORY_FLAG_NAME_MAX 16
-
-typedef uint16_t storyflag_t;
-typedef uint8_t storyflagval_t;
-
-typedef struct {
- char_t name[STORY_FLAG_NAME_MAX];
- storyflagval_t initial;
-} storyflagmap_t;
-
-extern storyflagmap_t STORY_FLAG_MAP[STORY_FLAG_COUNT_MAX];
-extern storyflag_t STORY_FLAG_COUNT;
-
-
-/**
- * Initializes the story subsystem, loading assets/story/storyflag.jsonc.
- *
- * @return Any error that occurs.
- */
-errorret_t storyInit(void);
-
-/**
- * Gets the ID of a story flag by name.
- *
- * @param name The name of the story flag to get.
- * @return The ID of the story flag, or STORY_FLAG_COUNT_MAX if not found.
- */
-storyflag_t storyGetFlagByName(const char_t *name);
-
-/**
- * Gets the name of a story flag by ID.
- *
- * @param flag The ID of the story flag to get.
- * @return The name of the story flag, or NULL if not found.
- */
-const char_t * storyGetNameByFlag(storyflag_t flag);
-
-/**
- * Gets the value of a story flag. Reads directly from the active save
- * slot (SAVE.slot.storyFlags) - flag values have no separate live copy.
- *
- * @param flag The ID of the story flag to get.
- * @return The value of the story flag, or 0 if not found.
- */
-storyflagval_t storyGetFlagValue(storyflag_t flag);
-
-/**
- * Sets the value of a story flag. Writes directly to the active save
- * slot (SAVE.slot.storyFlags) - flag values have no separate live copy.
- * Marks the save slot dirty.
- *
- * @param flag The ID of the story flag to set.
- * @param value The value to set the story flag to.
- */
-void storySetFlagValue(storyflag_t flag, storyflagval_t value);
-
-/**
- * Sets the value of a story flag by name. Writes directly to the active save
- * slot (SAVE.slot.storyFlags) - flag values have no separate live copy.
- * Marks the save slot dirty.
- *
- * @param name The name of the story flag to set.
- * @param value The value to set the story flag to.
- */
-void storySetFlagValueByName(const char_t *name, storyflagval_t value);
diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c
index 9c8ece44..1c5ab819 100644
--- a/src/dusk/save/save.c
+++ b/src/dusk/save/save.c
@@ -8,7 +8,6 @@
#include "save.h"
#include "util/memory.h"
#include "assert/assert.h"
-#include "rpg/cutscene/cutscenesystem.h"
#include "console/console.h"
save_t SAVE;
@@ -167,7 +166,7 @@ void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user) {
}
bool_t saveCanSave() {
- return CUTSCENE_SYSTEM.scene == NULL;
+ return true;
}
errorret_t saveSaveSettings() {
diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h
index 2d13e701..92078fbc 100644
--- a/src/dusk/save/save.h
+++ b/src/dusk/save/save.h
@@ -69,7 +69,7 @@ void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user);
/**
* Whether the game is in a state where saving is currently allowed.
- * Currently just checks that no cutscene is running.
+ * Currently always allowed.
*
* @return True if saving is currently allowed, false otherwise.
*/
diff --git a/src/dusk/save/slot/saveslot.c b/src/dusk/save/slot/saveslot.c
index cb966442..e989fd6c 100644
--- a/src/dusk/save/slot/saveslot.c
+++ b/src/dusk/save/slot/saveslot.c
@@ -19,19 +19,6 @@ errorret_t saveSlotInit(saveslot_t *slot) {
memoryZero(slot, sizeof(saveslot_t));
slot->version = 1;
- // Set the default story flags.
- for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
- slot->storyFlags[flag] = STORY_FLAG_MAP[flag].initial;
- }
-
- // Set the default party.
- errorChain(partyInit(&slot->party));
-
- // Set the default current cutscene.
- stringCopy(
- slot->currentCutscene, SAVE_SLOT_CUTSCENE_DEFAULT,
- sizeof(slot->currentCutscene) - 1
- );
errorOk();
}
@@ -39,23 +26,9 @@ errorret_t saveSlotInit(saveslot_t *slot) {
errorret_t saveSlotNewGame(void) {
errorChain(saveSlotInit(&SAVE.slot));
- stringCopy(
- SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
- sizeof(SAVE.slot.cachedData.mapName)
- );
-
errorOk();
}
-void saveSlotSetCurrentCutscene(const char_t *file) {
- assertNotNull(file, "File cannot be null");
-
- stringCopy(
- SAVE.slot.currentCutscene, file, sizeof(SAVE.slot.currentCutscene) - 1
- );
- SAVE.slotDirty = true;
-}
-
bool_t saveSlotInUse(saveslotcache_t *slot) {
assertNotNull(slot, "Slot cannot be null");
return slot->name[0] != '\0';
@@ -80,11 +53,7 @@ errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
assertNotNull(object, "Object cannot be null");
// saveSlotInit() now takes slot directly, so this can reset through it
- // rather than duplicating the memSet/version stub - saveSlotCurrent
- // ReadJSON() fully overwrites every field it sets anyway (including
- // party.members/order), so seeding a fresh party.jsonc roster here only
- // to replace it is wasted work, but keeps this in one reset path rather
- // than two subtly different ones.
+ // rather than duplicating the reset stub here.
errorChain(saveSlotInit(slot));
errorChain(saveSlotCurrentReadJSON(slot, object));
diff --git a/src/dusk/save/slot/saveslot.h b/src/dusk/save/slot/saveslot.h
index 6a934f08..9e80df22 100644
--- a/src/dusk/save/slot/saveslot.h
+++ b/src/dusk/save/slot/saveslot.h
@@ -7,9 +7,6 @@
#pragma once
#include "save/savedevice.h"
-#include "rpg/overworld/map.h"
-#include "rpg/battle/party.h"
-#include "rpg/story/story.h"
#include "time/timeepoch.h"
#include "asset/assetfile.h"
#include "yyjson.h"
@@ -19,15 +16,11 @@
#define SAVE_SLOT_COUNT 3
#endif
-#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld"
-#define SAVE_SLOT_CUTSCENE_NAME_MAX ASSET_FILE_NAME_MAX
-#define SAVE_SLOT_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
typedef struct {
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
dusktimeepoch_t time;
int32_t playerLevel;
- char_t mapName[MAP_NAME_MAX];
bool_t corrupt;
} saveslotcache_t;
@@ -35,47 +28,26 @@ typedef struct saveslot_s {
uint8_t version;
uint8_t dataType;
saveslotcache_t cachedData;
- party_t party;
- storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
- // Asset path of the cutscene currently in progress for this slot (e.g.
- // resumed on load) - defaults to SAVE_SLOT_CUTSCENE_DEFAULT on a fresh
- // slot (see saveSlotInit).
- char_t currentCutscene[SAVE_SLOT_CUTSCENE_NAME_MAX];
} saveslot_t;
/**
* Inits slot with the default state, this is functionally "new game" but
* will not set the player name, as that is what we use to determine if a
- * save slot is "in use" or not. Seeds the party roster from party.jsonc
- * (see partyInit).
+ * save slot is "in use" or not.
*
* @param slot The save slot to initialize.
- * @return Error if the starting party roster (party.jsonc) fails to
- * load/parse.
+ * @return Error state if any.
*/
errorret_t saveSlotInit(saveslot_t *slot);
/**
- * Sets up the current save slot (SAVE.slot) for a brand new game: calls
- * saveSlotInit (which seeds the starting party roster) and sets the
- * default starting map. Use this (not saveSlotInit directly) whenever a
- * new save is actually being created for a player. Callers creating a
- * new save at a specific index should set SAVE.slotCurrent first.
+ * Sets up the current save slot (SAVE.slot) for a brand new game. Callers
+ * creating a new save at a specific index should set SAVE.slotCurrent first.
*
* @return Error if saveSlotInit fails.
*/
errorret_t saveSlotNewGame(void);
-/**
- * Sets the current save slot's (SAVE.slot) in-progress cutscene and
- * dirties the save (SAVE.slotDirty), same as storySetFlagValue - so the
- * next save write persists it. Use this (not writing SAVE.slot.
- * currentCutscene directly) anywhere gameplay needs to change it.
- *
- * @param file Asset path of the cutscene now in progress, or "" for none.
- */
-void saveSlotSetCurrentCutscene(const char_t *file);
-
/**
* Checks if the save slot is in use, this is determined by checking if the
* player name is set or not.
diff --git a/src/dusk/save/slot/saveslotcurrent.c b/src/dusk/save/slot/saveslotcurrent.c
index 76c199e4..5e433ba2 100644
--- a/src/dusk/save/slot/saveslotcurrent.c
+++ b/src/dusk/save/slot/saveslotcurrent.c
@@ -28,18 +28,6 @@ errorret_t saveSlotCurrentWriteJSON(
stringCopy(ver1.name, slot->cachedData.name, sizeof(ver1.name));
ver1.time = slot->cachedData.time;
ver1.playerLevel = slot->cachedData.playerLevel;
- stringCopy(ver1.mapName, slot->cachedData.mapName, sizeof(ver1.mapName));
-
- memoryCopy(
- ver1.partyMembers, slot->party.members, sizeof(ver1.partyMembers)
- );
- memoryCopy(ver1.partyOrder, slot->party.order, sizeof(ver1.partyOrder));
-
- memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags));
-
- stringCopy(
- ver1.currentCutscene, slot->currentCutscene, sizeof(ver1.currentCutscene)
- );
errorChain(saveSlotVer1WriteJSON(&ver1, doc, object));
errorOk();
@@ -59,21 +47,6 @@ errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) {
stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name));
slot->cachedData.time = ver1.time;
slot->cachedData.playerLevel = ver1.playerLevel;
- stringCopy(
- slot->cachedData.mapName, ver1.mapName, sizeof(slot->cachedData.mapName)
- );
-
- memoryCopy(
- slot->party.members, ver1.partyMembers, sizeof(slot->party.members)
- );
- memoryCopy(slot->party.order, ver1.partyOrder, sizeof(slot->party.order));
-
- memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags));
-
- stringCopy(
- slot->currentCutscene, ver1.currentCutscene,
- sizeof(slot->currentCutscene)
- );
errorOk();
}
diff --git a/src/dusk/save/slot/saveslotver1.c b/src/dusk/save/slot/saveslotver1.c
index 247166b6..6e9d5ef8 100644
--- a/src/dusk/save/slot/saveslotver1.c
+++ b/src/dusk/save/slot/saveslotver1.c
@@ -22,26 +22,6 @@ errorret_t saveSlotVer1WriteJSON(
writeString("name", slot->name);
writeTime("time", slot->time);
writeInt32("playerLevel", slot->playerLevel);
- writeString("mapName", slot->mapName);
-
- yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc);
- for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
- yyjson_mut_val *memberObj =
- battleFighterSerialize(&slot->partyMembers[i], doc);
- yyjson_mut_arr_add_val(partyMembersArr, memberObj);
- }
- writeValue("partyMembers", partyMembersArr);
-
- writeUInt8Array("partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX);
-
- yyjson_mut_val *storyFlagsObj = yyjson_mut_obj_add_obj(doc, object, "storyFlags");
- for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
- yyjson_mut_obj_add_uint(
- doc, storyFlagsObj, storyGetNameByFlag(flag), slot->storyFlags[flag]
- );
- }
-
- writeString("currentCutscene", slot->currentCutscene);
errorOk();
}
@@ -59,47 +39,6 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
readTime("time", slot->time);
readInt32("playerLevel", slot->playerLevel, 1);
- requireStrMin("mapName", 1);
- readString(
- "mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1
- );
-
- requireArrayOfSize(
- "partyMembers", partyMembersArr, PARTY_MEMBER_COUNT_MAX
- );
-
- size_t memberIdx, memberLen;
- yyjson_val *memberObj;
- yyjson_arr_foreach(
- partyMembersArr, memberIdx, memberLen, memberObj
- ) {
- battlefighter_t *member = &slot->partyMembers[memberIdx];
- member->id = (uint8_t)memberIdx;
- errorChain(battleFighterParse(member, memberObj));
- }
-
- readUInt8Array(
- "partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX,
- PARTY_ORDER_EMPTY
- );
-
- yyjson_val *storyFlagsObj = yyjson_obj_get(object, "storyFlags");
- if(storyFlagsObj != NULL) {
- yyjson_obj_iter iter = yyjson_obj_iter_with(storyFlagsObj);
- yyjson_val *flagKey;
- while((flagKey = yyjson_obj_iter_next(&iter))) {
- storyflag_t flag = storyGetFlagByName(yyjson_get_str(flagKey));
- if(flag >= STORY_FLAG_COUNT) continue;// Flag no longer exists, ignore
-
- slot->storyFlags[flag] =
- (storyflagval_t)yyjson_get_uint(yyjson_obj_iter_get_val(flagKey));
- }
- }
-
- readString(
- "currentCutscene", slot->currentCutscene, SAVE_SLOT_VER1_CUTSCENE_DEFAULT,
- SAVE_SLOT_VER1_CUTSCENE_NAME_MAX - 1
- );
errorOk();
}
diff --git a/src/dusk/save/slot/saveslotver1.h b/src/dusk/save/slot/saveslotver1.h
index 77040c74..6ccbb672 100644
--- a/src/dusk/save/slot/saveslotver1.h
+++ b/src/dusk/save/slot/saveslotver1.h
@@ -9,37 +9,13 @@
#include "error/error.h"
#include "time/timeepoch.h"
#include "save/slot/saveslotver.h"
-#include "rpg/battle/party.h"
-#include "rpg/story/story.h"
#include "yyjson.h"
// This schema's own frozen copies of save/slot/saveslot.h's field sizes -
-// never reference SAVE_SLOT_NAME_LENGTH/MAP_NAME_MAX (or any other live
+// never reference SAVE_SLOT_NAME_LENGTH (or any other live
// constant) here. Once shipped, this file's on-disk shape must never change
-// underneath old save files, even if the live engine constants do. The
-// party arrays below are the exception - saveSlotVer1ReadJSON() checks their
-// JSON array lengths explicitly, so PARTY_MEMBER_COUNT_MAX/
-// PARTY_ACTIVE_SIZE_MAX (rpg/battle/party.h) can be referenced directly.
-// storyFlags is a further exception, for a different reason - it's written
-// keyed by flag name (not position), so its on-disk shape is decoupled from
-// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no
-// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks.
-//
-// partyMembers is a THIRD exception, deliberately broken pre-release: its
-// per-member shape now comes from battleFighterSerialize/battleFighterParse
-// (rpg/battle/fighter/battlefighter.h) - the shared fighter JSON format
-// used everywhere else (party.jsonc, BATTLE_START enemies) - instead of its
-// own hand-rolled field set. This is a real on-disk break (different key
-// names, no more "statAttack" etc.) with no migration - an old v1 save
-// simply reads back as corrupt. Acceptable only because nothing has
-// shipped yet; don't repeat this once real saves exist without a version
-// bump instead.
+// underneath old save files, even if the live engine constants do.
#define SAVE_SLOT_VER1_NAME_LENGTH 8
-#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
-#define SAVE_SLOT_VER1_CUTSCENE_NAME_MAX 128
-// Own frozen copy of save/slot/saveslot.h's SAVE_SLOT_CUTSCENE_DEFAULT, for
-// the same reason as the sizes above - never reference the live constant.
-#define SAVE_SLOT_VER1_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
// Independent from saveslotcache_t (save/slot/saveslot.h) by design - this
// is what version 1 actually put on disk, frozen forever. saveSlotCurrent
@@ -50,14 +26,6 @@ typedef struct saveslotver1_s {
char_t name[SAVE_SLOT_VER1_NAME_LENGTH + 1];
dusktimeepoch_t time;
int32_t playerLevel;
- char_t mapName[SAVE_SLOT_VER1_MAP_NAME_MAX];
-
- battlefighter_t partyMembers[PARTY_MEMBER_COUNT_MAX];
- uint8_t partyOrder[PARTY_ACTIVE_SIZE_MAX];
-
- storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
-
- char_t currentCutscene[SAVE_SLOT_VER1_CUTSCENE_NAME_MAX];
} saveslotver1_t;
/**
diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt
index 8273e118..4fef675a 100644
--- a/src/dusk/scene/CMakeLists.txt
+++ b/src/dusk/scene/CMakeLists.txt
@@ -5,11 +5,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
+ node.c
scene.c
scenetype.c
)
-# Subdirs
-add_subdirectory(initial)
-add_subdirectory(overworld)
-add_subdirectory(battle)
\ No newline at end of file
diff --git a/src/dusk/scene/battle/CMakeLists.txt b/src/dusk/scene/battle/CMakeLists.txt
deleted file mode 100644
index a743129d..00000000
--- a/src/dusk/scene/battle/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- scenebattle.c
-)
diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c
deleted file mode 100644
index 190db10a..00000000
--- a/src/dusk/scene/battle/scenebattle.c
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "scenebattle.h"
-#include "assert/assert.h"
-#include "display/display.h"
-#include "display/displaystate.h"
-#include "display/shader/shader.h"
-#include "display/shader/shaderunlit.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "display/color.h"
-#include "scene/scene.h"
-
-errorret_t sceneBattleInit(scenedata_t *sceneData) {
- errorOk();
-}
-
-errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
- battleUpdate();
- errorOk();
-}
-
-errorret_t sceneBattleRender(scenedata_t *sceneData) {
- scenebattle_t *battle = &sceneData->battle;
-
- sceneBattleCameraUpdateProjection(battle);
- sceneBattleCameraUpdateEye(battle);
-
- errorChain(shaderBind(&SHADER_UNLIT));
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
- ));
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, battle->projection
- ));
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_VIEW, battle->eye
- ));
-
- errorChain(displaySetState((displaystate_t){
- .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
- }));
-
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- errorChain(sceneBattleDrawFighter(i));
- }
-
- errorOk();
-}
-
-errorret_t sceneBattleDispose(scenedata_t *sceneData) {
- errorOk();
-}
-
-void sceneBattleCameraUpdateProjection(scenebattle_t *battle) {
- glm_perspective(
- glm_rad(SCENE_BATTLE_CAMERA_FOV),
- SCREEN.aspect,
- SCENE_BATTLE_CAMERA_NEAR,
- SCENE_BATTLE_CAMERA_FAR,
- battle->projection
- );
-}
-
-void sceneBattleCameraUpdateEye(scenebattle_t *battle) {
- glm_lookat(
- (vec3){ 0.0f, 4.0f, 7.0f },
- (vec3){ 0.0f, 0.5f, 0.0f },
- (vec3){ 0.0f, 1.0f, 0.0f },
- battle->eye
- );
-}
-
-void sceneBattleWorldToScreen(
- scenebattle_t *battle,
- vec3 renderPos,
- vec2 out
-) {
- assertNotNull(battle, "battle cannot be NULL");
- assertNotNull(renderPos, "renderPos cannot be NULL");
- assertNotNull(out, "out cannot be NULL");
-
- mat4 viewProj;
- glm_mat4_mul(battle->projection, battle->eye, viewProj);
-
- vec4 viewport = {
- 0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
- };
- vec3 window;
- glm_project(renderPos, viewProj, viewport, window);
-
- out[0] = window[0];
- out[1] = (float_t)SCREEN.height - window[1];
-}
-
-void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) {
- assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds");
- const battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- assertNotNull(fighter, "Fighter cannot be NULL");
- assertNotNull(out, "out cannot be NULL");
-
- // Ordinal (and count) of this fighter among its living-slot teammates,
- // in slot order, so each team lays out as an evenly spaced row.
- uint8_t ordinal = 0;
- uint8_t teamCount = 0;
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- const battlefighter_t *candidate = BATTLE.fighters[i];
- if(candidate == NULL) continue;
- if(candidate->status == BATTLE_FIGHTER_STATUS_NULL) continue;
- if(candidate->team != fighter->team) continue;
- if(i < fighterIndex) ordinal++;
- teamCount++;
- }
-
- const float_t x =
- ((float_t)ordinal - ((float_t)(teamCount - 1) * 0.5f)) *
- SCENE_BATTLE_FIGHTER_SPACING;
- const float_t z = fighter->team == BATTLE_FIGHTER_TEAM_ALLY
- ? SCENE_BATTLE_ALLY_Z : SCENE_BATTLE_ENEMY_Z;
-
- out[0] = x;
- out[1] = 0.0f;
- out[2] = z;
-}
-
-errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) {
- assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds");
- const battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
- if(fighter == NULL) errorOk();
- if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk();
-
- vec3 position;
- sceneBattleGetFighterPosition(fighterIndex, position);
-
- spritebatchsprite_t sprite;
- glm_vec3_copy(position, sprite.min);
- glm_vec3_copy(position, sprite.max);
- glm_vec3_add(sprite.max, (vec3){ 1.0f, 1.0f, 0.0f }, sprite.max);
- glm_vec2_copy((vec2){ 0.0f, 0.0f }, sprite.uvMin);
- glm_vec2_copy((vec2){ 1.0f, 1.0f }, sprite.uvMax);
-
- color_t color;
- if(fighter->status == BATTLE_FIGHTER_STATUS_DEAD) {
- color = color4b(96, 96, 96, 255);
- } else if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) {
- color = COLOR_BLUE;
- } else {
- color = COLOR_RED;
- }
-
- shadermaterial_t material = {
- .unlit = { .color = color, .texture = NULL }
- };
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- errorChain(spriteBatchFlush());
-
- errorOk();
-}
diff --git a/src/dusk/scene/battle/scenebattle.h b/src/dusk/scene/battle/scenebattle.h
deleted file mode 100644
index bfe0e98e..00000000
--- a/src/dusk/scene/battle/scenebattle.h
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "scene/scenebase.h"
-#include "rpg/battle/battle.h"
-
-// Fixed mock camera, "to begin": looking down at the arena from above and
-// in front. Not yet driven by anything (no shake, no per-fighter framing).
-#define SCENE_BATTLE_CAMERA_FOV 45.0f
-#define SCENE_BATTLE_CAMERA_NEAR 0.1f
-#define SCENE_BATTLE_CAMERA_FAR 100.0f
-
-// Fighter row layout, in render-space units. Allies stand closer to the
-// camera (+Z), enemies further away (-Z); each team is spread evenly
-// along X, centered on the origin.
-#define SCENE_BATTLE_FIGHTER_SPACING 2.0f
-#define SCENE_BATTLE_ALLY_Z 2.5f
-#define SCENE_BATTLE_ENEMY_Z (-2.5f)
-
-typedef struct {
- mat4 eye;
- mat4 projection;
-} scenebattle_t;
-
-/**
- * Initializes the battle scene. The battle itself (BATTLE global) is
- * expected to already be started, e.g. by a StartBattle cutscene item.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the init failed, or errorOk() if it succeeded.
- */
-errorret_t sceneBattleInit(scenedata_t *sceneData);
-
-/**
- * Updates the battle scene, ticking the battle simulation.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the update failed, or errorOk() if it succeeded.
- */
-errorret_t sceneBattleUpdate(scenedata_t *sceneData);
-
-/**
- * Renders the battle scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the render failed, or errorOk() if it succeeded.
- */
-errorret_t sceneBattleRender(scenedata_t *sceneData);
-
-/**
- * Disposes the battle scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the dispose failed, or errorOk() if it succeeded.
- */
-errorret_t sceneBattleDispose(scenedata_t *sceneData);
-
-/**
- * Recomputes the battle camera's projection matrix from the current
- * screen aspect ratio, storing it in battle->projection.
- *
- * @param battle The battle scene data to update.
- */
-void sceneBattleCameraUpdateProjection(scenebattle_t *battle);
-
-/**
- * Recomputes the battle camera's eye/view matrix, storing it in
- * battle->eye. Fixed for now -- doesn't yet track anything.
- *
- * @param battle The battle scene data to update.
- */
-void sceneBattleCameraUpdateEye(scenebattle_t *battle);
-
-/**
- * Converts a render-space position to screen-space pixel coordinates,
- * using the battle camera's current eye and projection matrices.
- *
- * @param battle The battle scene data holding the camera matrices.
- * @param renderPos The render-space position to convert.
- * @param out Output vec2 filled with the screen-space pixel position.
- */
-void sceneBattleWorldToScreen(
- scenebattle_t *battle,
- vec3 renderPos,
- vec2 out
-);
-
-/**
- * Computes the render-space position of a fighter's feet, laying out
- * each team as an evenly spaced row facing the camera.
- *
- * @param fighterIndex Index into BATTLE.fighters.
- * @param out Output vec3 filled with the fighter's render-space position.
- */
-void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out);
-
-/**
- * Draws a single fighter as a colored 1x1 quad, skipping empty slots.
- * Color differentiates team (ally/enemy) and status (dead fighters are
- * drawn dimmed).
- *
- * @param fighterIndex Index into BATTLE.fighters.
- * @return An error if drawing failed, or errorOk() if it succeeded.
- */
-errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex);
diff --git a/src/dusk/scene/initial/CMakeLists.txt b/src/dusk/scene/initial/CMakeLists.txt
deleted file mode 100644
index 97ded407..00000000
--- a/src/dusk/scene/initial/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- sceneinitial.c
-)
diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c
deleted file mode 100644
index 29fada88..00000000
--- a/src/dusk/scene/initial/sceneinitial.c
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "scene/scene.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "error/error.h"
-#include "display/screen/screen.h"
-#include "console/console.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-errorret_t sceneInitialInit(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
-
- SCREEN.background = COLOR_BLACK;
- cutsceneSystemLoad("cutscenes/initial.jsonc");
-
- errorOk();
-}
-
-errorret_t sceneInitialUpdate(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
-
-errorret_t sceneInitialRender(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
-
-errorret_t sceneInitialDispose(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
diff --git a/src/dusk/scene/initial/sceneinitial.h b/src/dusk/scene/initial/sceneinitial.h
deleted file mode 100644
index 321f2da2..00000000
--- a/src/dusk/scene/initial/sceneinitial.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "scene/scenebase.h"
-
-typedef struct {
- // uint32_t callbackState;
- void *nothing;
-} sceneinitial_t;
-
-/**
- * Initialises the initial (boot) scene - kicks off the save
- * availability/existence check that decides which prompt, if any, to
- * show before proceeding to the main menu.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the init failed, or errorOk() if it succeeded.
- */
-errorret_t sceneInitialInit(scenedata_t *sceneData);
-
-/**
- * Updates the initial scene. Currently a no-op - all the work happens in
- * save callbacks and the two modal UI elements' own button handling.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the update failed, or errorOk() if it succeeded.
- */
-errorret_t sceneInitialUpdate(scenedata_t *sceneData);
-
-/**
- * Renders the initial scene. Currently a no-op - the modals draw
- * themselves via the global UI element pipeline.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the render failed, or errorOk() if it succeeded.
- */
-errorret_t sceneInitialRender(scenedata_t *sceneData);
-
-/**
- * Disposes the initial scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the dispose failed, or errorOk() if it succeeded.
- */
-errorret_t sceneInitialDispose(scenedata_t *sceneData);
diff --git a/src/dusk/scene/node.c b/src/dusk/scene/node.c
new file mode 100644
index 00000000..90d24131
--- /dev/null
+++ b/src/dusk/scene/node.c
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#include "node.h"
+#include "assert/assert.h"
+#include "util/memory.h"
+
+void nodeInit(node_t *node) {
+ assertNotNull(node, "Node cannot be NULL");
+ memoryZero(node, sizeof(node_t));
+}
diff --git a/src/dusk/scene/node.h b/src/dusk/scene/node.h
new file mode 100644
index 00000000..979eb371
--- /dev/null
+++ b/src/dusk/scene/node.h
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "error/error.h"
+
+typedef struct node_s node_t;
+
+typedef struct node_s {
+ node_t *parent;
+ node_t *firstChild;
+ node_t *nextSibling;
+} node_t;
+
+/**
+ * Initializes a node to an empty, detached state.
+ *
+ * @param node The node to initialize.
+ */
+void nodeInit(node_t *node);
diff --git a/src/dusk/scene/overworld/CMakeLists.txt b/src/dusk/scene/overworld/CMakeLists.txt
deleted file mode 100644
index 6038dc07..00000000
--- a/src/dusk/scene/overworld/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- sceneoverworld.c
-)
\ No newline at end of file
diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c
deleted file mode 100644
index 6a7214b5..00000000
--- a/src/dusk/scene/overworld/sceneoverworld.c
+++ /dev/null
@@ -1,264 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "scene/scene.h"
-#include "console/console.h"
-#include "assert/assert.h"
-
-#include "display/display.h"
-#include "display/displaystate.h"
-#include "display/shader/shader.h"
-#include "display/screen/screen.h"
-#include "display/shader/shaderunlit.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/texture/texture.h"
-
-#include "rpg/overworld/map.h"
-#include "rpg/entity/entity.h"
-#include "rpg/rpgcamera.h"
-
-#include "asset/loader/assetloader.h"
-#include "asset/loader/assetentry.h"
-
-#include "util/memory.h"
-
-errorret_t sceneOverworldInit(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
-
-errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
-
-errorret_t sceneOverworldRender(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
-
- sceneoverworld_t *overworld = &sceneData->overworld;
- sceneOverworldCullUpdate(overworld);
-
-
- // Overworld camera
- errorChain(shaderBind(&SHADER_UNLIT));
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
- ));
-
- rpgCameraUpdateProjection();
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection
- ));
-
- rpgCameraUpdateEye();
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye
- ));
-
- // Chunk tiles
- errorChain(displaySetState((displaystate_t){
- .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
- }));
- errorChain(sceneOverworldDrawChunksBase(overworld));
-
- // Entities
- errorChain(displaySetState((displaystate_t){
- .flags = DISPLAY_STATE_FLAG_CULL
- }));
-
- for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
- entity_t *ent = &ENTITIES[i];
- if(ent->type == ENTITY_TYPE_NULL) continue;
- errorChain(sceneOverworldDrawEntity(overworld, ent));
- }
-
- // Chunk props
- errorChain(displaySetState((displaystate_t){
- .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
- }));
- errorChain(sceneOverworldDrawChunksProps(overworld));
-
- // Weather effects
- // errorChain(shaderSetMatrix(
- // &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
- // ));
- // errorChain(shaderSetMatrix(
- // &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
- // ));
- // errorChain(displaySetState((displaystate_t){
- // .flags = DISPLAY_STATE_FLAG_BLEND
- // }));
-
- // spritebatchsprite_t skyboxSprite;
- // memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t));
- // skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi);
- // skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi);
- // shadermaterial_t skyboxMaterial = {
- // .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL }
- // };
- // errorChain(
- // spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial)
- // );
- // errorChain(spriteBatchFlush());
-
- errorOk();
-}
-
-void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
- rpgCameraGetPosition(overworld->cullTarget);
-
- const float_t worldH =
- (float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
- const float_t worldW = worldH * SCREEN.aspect;
-
- overworld->cullHalfRangeX =
- (worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
- overworld->cullHalfRangeY =
- (worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
-}
-
-bool_t sceneOverworldChunkShouldRender(
- const sceneoverworld_t *overworld,
- const chunk_t *chunk
-) {
- worldpos_t worldPos;
- chunkPosToWorldPos(&chunk->position, &worldPos);
-
- const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
- const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
-
- if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
- overworld->cullHalfRangeX) return false;
- if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
- overworld->cullHalfRangeY) return false;
-
- return true;
-}
-
-bool_t sceneOverworldEntityShouldRender(
- const sceneoverworld_t *overworld,
- const entity_t *ent
-) {
- if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
- overworld->cullHalfRangeX) return false;
- if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
- overworld->cullHalfRangeY) return false;
-
- return true;
-}
-
-errorret_t sceneOverworldDrawEntity(
- const sceneoverworld_t *overworld,
- entity_t *ent
-) {
- if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
-
- spritebatchsprite_t sprite;
- glm_vec3_copy(ent->renderPosition, sprite.min);
- glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight
- glm_vec3_copy(sprite.min, sprite.max);
- glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
- glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
- glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
-
- color_t color;
- switch(ent->direction) {
- case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
- case ENTITY_DIR_EAST: color = COLOR_RED; break;
- case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
- case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
- default: color = COLOR_CYAN; break;
- }
-
- shadermaterial_t material = {
- .unlit = { .color = color, .texture = NULL }
- };
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- errorChain(spriteBatchFlush());
-
- errorOk();
-}
-
-errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- chunk_t *chunk = MAP.chunkOrder[i];
- if(chunk == NULL) continue;
- if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
- if(chunk->meshCount == 0) continue;
- if(chunk->modelEntries[0] == NULL) continue;
- if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
-
- assetmodeloutput_t *model = &chunk->modelEntries[0]->data.model;
- if(model->meshEntry == NULL) continue;
-
- texture_t *tex = model->texEntry != NULL
- ? &model->texEntry->data.texture
- : NULL;
-
- shadermaterial_t mat = {
- .unlit = { .color = model->color, .texture = tex }
- };
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[0]
- ));
- errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat));
- errorChain(meshDraw(
- &model->meshEntry->data.mesh.mesh, 0, -1
- ));
- }
-
- // Restore identity model so subsequent renders (e.g. entities) are
- // not affected by the last chunk transform.
- mat4 identity;
- glm_mat4_identity(identity);
- errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity));
-
- errorOk();
-}
-
-errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- chunk_t *chunk = MAP.chunkOrder[i];
- if(chunk == NULL) continue;
- if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
-
- for(uint8_t m = 1; m < chunk->meshCount; m++) {
- if(chunk->modelEntries[m] == NULL) continue;
- if(chunk->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) continue;
-
- assetmodeloutput_t *model = &chunk->modelEntries[m]->data.model;
- if(model->meshEntry == NULL) continue;
-
- texture_t *tex = model->texEntry != NULL
- ? &model->texEntry->data.texture
- : NULL;
-
- shadermaterial_t mat = {
- .unlit = { .color = model->color, .texture = tex }
- };
- errorChain(shaderSetMatrix(
- &SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m]
- ));
- errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat));
- errorChain(meshDraw(
- &model->meshEntry->data.mesh.mesh, 0, -1
- ));
- }
- }
-
- // Restore identity model in case anything renders after props.
- mat4 identity;
- glm_mat4_identity(identity);
- errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity));
-
- errorOk();
-}
-
-errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
- assertNotNull(sceneData, "Scene data cannot be null");
- errorOk();
-}
diff --git a/src/dusk/scene/overworld/sceneoverworld.h b/src/dusk/scene/overworld/sceneoverworld.h
deleted file mode 100644
index 048e20d0..00000000
--- a/src/dusk/scene/overworld/sceneoverworld.h
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "scene/scenebase.h"
-#include "rpg/overworld/chunk.h"
-#include "rpg/entity/entity.h"
-
-typedef struct {
- // Cached per-frame chunk culling bounds, refreshed by
- // sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
- // does not need to recompute them for every chunk.
- vec3 cullTarget;
- float_t cullHalfRangeX;
- float_t cullHalfRangeY;
-} sceneoverworld_t;
-
-// Extra world-space margin, in tiles, added on top of the rough
-// screen-size skin below - cheap insurance against the approximation
-// culling a chunk that is actually still just in view. Y gets its own
-// value since the camera's tilt makes the vertical check less exact.
-#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
-#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
-
-// The camera is tilted between the ground-depth (Y) and height (Z)
-// axes, so a chunk's on-screen vertical extent also depends on its Z
-// layers. Rather than projecting every corner, fold that into a
-// generous skin margin on the depth check.
-#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
- ((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
-#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
- ((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
- SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
-
-/**
- * Initialises the overworld scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the init failed, or errorOk() if it succeeded.
- */
-errorret_t sceneOverworldInit(scenedata_t *sceneData);
-
-/**
- * Updates the overworld scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the update failed, or errorOk() if it succeeded.
- */
-errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
-
-/**
- * Refreshes the chunk culling bounds cached on the overworld scene data,
- * from the current camera position and screen dimensions. Must be
- * called once per frame before sceneOverworldChunkShouldRender().
- *
- * @param overworld The overworld scene data to update.
- */
-void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
-
-/**
- * Roughly checks whether a chunk falls within the visible screen area,
- * based on chunk size and screen dimensions. Cheap approximation, not
- * an exact frustum check - includes a generous skin margin to account
- * for the camera's angle tilting height/depth onto the screen's
- * vertical axis, so it may pass some chunks that are actually just out
- * of view but will never wrongly cull one that is visible.
- *
- * @param overworld The overworld scene data holding the cached culling
- * bounds, as refreshed by sceneOverworldCullUpdate().
- * @param chunk The chunk to check.
- * @return true if the chunk should be rendered, false otherwise.
- */
-bool_t sceneOverworldChunkShouldRender(
- const sceneoverworld_t *overworld,
- const chunk_t *chunk
-);
-
-/**
- * Draws the base (tile) mesh of every loaded chunk, with normal depth
- * testing. Must be called before entities are rendered.
- *
- * @param overworld The overworld scene data holding the cached culling
- * bounds, as refreshed by sceneOverworldCullUpdate().
- * @return An error if drawing failed, or errorOk() on success.
- */
-errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
-
-/**
- * Draws every loaded chunk's additional meshes (trees, buildings, etc),
- * with normal depth testing so they correctly occlude entities standing
- * beneath them. Must be called after entities are rendered.
- *
- * @param overworld The overworld scene data holding the cached culling
- * bounds, as refreshed by sceneOverworldCullUpdate().
- * @return An error if drawing failed, or errorOk() on success.
- */
-errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
-
-/**
- * Roughly checks whether an entity falls within the visible screen
- * area. Reuses the same cached chunk culling bounds, since an entity
- * is just a point within that same space - deliberately cheap, a
- * couple of subtractions and comparisons, since it runs per entity
- * every frame and it is not worth spending more math than that to
- * avoid drawing one that is slightly off-screen.
- *
- * @param overworld The overworld scene data holding the cached culling
- * bounds, as refreshed by sceneOverworldCullUpdate().
- * @param ent The entity to check.
- * @return true if the entity should be rendered, false otherwise.
- */
-bool_t sceneOverworldEntityShouldRender(
- const sceneoverworld_t *overworld,
- const entity_t *ent
-);
-
-/**
- * Draws a single entity as a sprite, colored by its facing direction.
- * Skips drawing (without error) if the entity should not render.
- *
- * @param overworld The overworld scene data holding the cached culling
- * bounds, as refreshed by sceneOverworldCullUpdate().
- * @param ent The entity to draw.
- * @return An error if drawing failed, or errorOk() on success.
- */
-errorret_t sceneOverworldDrawEntity(
- const sceneoverworld_t *overworld,
- entity_t *ent
-);
-
-/**
- * Renders the overworld scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the render failed, or errorOk() if it succeeded.
- */
-errorret_t sceneOverworldRender(scenedata_t *sceneData);
-
-/**
- * Disposes the overworld scene.
- *
- * @param sceneData The scene data used for this scene.
- * @return An error if the dispose failed, or errorOk() if it succeeded.
- */
-errorret_t sceneOverworldDispose(scenedata_t *sceneData);
\ No newline at end of file
diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c
index 72c4e9d9..1873644e 100644
--- a/src/dusk/scene/scene.c
+++ b/src/dusk/scene/scene.c
@@ -10,7 +10,6 @@
#include "display/screen/screen.h"
#include "display/shader/shaderunlit.h"
#include "display/display.h"
-#include "ui/ui.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "console/console.h"
@@ -98,7 +97,6 @@ errorret_t sceneRender(void) {
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
- errorChain(uiRender());
errorOk();
}
diff --git a/src/dusk/scene/scenetype.c b/src/dusk/scene/scenetype.c
index 3dad6627..8cd954fc 100644
--- a/src/dusk/scene/scenetype.c
+++ b/src/dusk/scene/scenetype.c
@@ -8,27 +8,6 @@
#include "scenetype.h"
scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
- [SCENE_TYPE_NULL] = { 0 },
-
- [SCENE_TYPE_INITIAL] = {
- .init = sceneInitialInit,
- .update = sceneInitialUpdate,
- .render = sceneInitialRender,
- .dispose = sceneInitialDispose
- },
-
- [SCENE_TYPE_OVERWORLD] = {
- .init = sceneOverworldInit,
- .update = sceneOverworldUpdate,
- .render = sceneOverworldRender,
- .dispose = sceneOverworldDispose
- },
-
- [SCENE_TYPE_BATTLE] = {
- .init = sceneBattleInit,
- .update = sceneBattleUpdate,
- .render = sceneBattleRender,
- .dispose = sceneBattleDispose
- },
+ [SCENE_TYPE_NULL] = { 0 }
};
diff --git a/src/dusk/scene/scenetype.h b/src/dusk/scene/scenetype.h
index 677959ee..5b3fc1d3 100644
--- a/src/dusk/scene/scenetype.h
+++ b/src/dusk/scene/scenetype.h
@@ -7,14 +7,9 @@
#pragma once
#include "scene/scenebase.h"
-#include "scene/initial/sceneinitial.h"
-#include "scene/overworld/sceneoverworld.h"
-#include "scene/battle/scenebattle.h"
typedef union scenedata_u {
- sceneinitial_t initial;
- sceneoverworld_t overworld;
- scenebattle_t battle;
+ uint8_t unused;
} scenedata_t;
typedef errorret_t (*scenecallback_t)(scenedata_t *);
@@ -29,10 +24,6 @@ typedef struct {
typedef enum {
SCENE_TYPE_NULL,
- SCENE_TYPE_INITIAL,
- SCENE_TYPE_OVERWORLD,
- SCENE_TYPE_BATTLE,
-
SCENE_TYPE_COUNT
} scenetype_t;
diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt
deleted file mode 100644
index ac7639b2..00000000
--- a/src/dusk/ui/CMakeLists.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-add_subdirectory(debug)
-add_subdirectory(dialog)
-add_subdirectory(screen)
-add_subdirectory(focus)
-add_subdirectory(overlay)
-add_subdirectory(rpg)
-add_subdirectory(transition)
-add_subdirectory(widget)
-
-# Sources
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- ui.c
- uielement.c
- uielementlist.c
- # uitextbox.c
-)
\ No newline at end of file
diff --git a/src/dusk/ui/debug/CMakeLists.txt b/src/dusk/ui/debug/CMakeLists.txt
deleted file mode 100644
index 52d8095a..00000000
--- a/src/dusk/ui/debug/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uiconsole.c
- uicutscene.c
- uifps.c
- uiplayerpos.c
-)
diff --git a/src/dusk/ui/debug/uiconsole.c b/src/dusk/ui/debug/uiconsole.c
deleted file mode 100644
index 631bd1fd..00000000
--- a/src/dusk/ui/debug/uiconsole.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiconsole.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/spritebatch/spritebatch.h"
-
-uiconsole_t UICONSOLE;
-
-errorret_t uiConsoleInit(void) {
- float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight;
-
- for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
- uiLabelInit(
- &UICONSOLE.labels[i],
- CONSOLE.line[i],
- UICONSOLE.sprites[i], CONSOLE_LINE_MAX
- );
- uiLabelSetX(&UICONSOLE.labels[i], (float_t)SCREEN.scanX);
- uiLabelSetY(
- &UICONSOLE.labels[i], (float_t)SCREEN.scanY + lineH * (float_t)i
- );
- }
-
- errorOk();
-}
-
-errorret_t uiConsoleDraw(void) {
- if(!CONSOLE.visible) errorOk();
-
- for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
- UICONSOLE.labels[i].dirty = true;
- errorChain(uiLabelRender(&UICONSOLE.labels[i], COLOR_RED));
- }
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/debug/uiconsole.h b/src/dusk/ui/debug/uiconsole.h
deleted file mode 100644
index be745ab3..00000000
--- a/src/dusk/ui/debug/uiconsole.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "console/console.h"
-#include "ui/widget/uilabel.h"
-
-typedef struct {
- uilabel_t labels[CONSOLE_HISTORY_MAX];
- spritebatchsprite_t sprites[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
-} uiconsole_t;
-
-extern uiconsole_t UICONSOLE;
-
-/**
- * Initializes the console's history labels.
- *
- * @return Any error that occurs.
- */
-errorret_t uiConsoleInit(void);
-
-/**
- * Renders the console history into the scan-safe area.
- * No-ops when the console is not visible.
- *
- * @return Any error that occurs.
- */
-errorret_t uiConsoleDraw(void);
diff --git a/src/dusk/ui/debug/uicutscene.c b/src/dusk/ui/debug/uicutscene.c
deleted file mode 100644
index 94bdf321..00000000
--- a/src/dusk/ui/debug/uicutscene.c
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uicutscene.h"
-#include "util/string.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/color.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/cutscene/item/cutsceneitem.h"
-
-uicutscene_t UICUTSCENE;
-
-errorret_t uiCutsceneInit() {
- uiLabelInit(
- &UICUTSCENE.countLabel,
- UICUTSCENE.countText,
- UICUTSCENE.countSprites, UI_CUTSCENE_SPRITES_MAX
- );
- uiLabelSetX(&UICUTSCENE.countLabel, (float_t)SCREEN.scanX);
- uiLabelSetY(&UICUTSCENE.countLabel, (float_t)SCREEN.scanY +
- (float_t)FONT_DEFAULT.tileset->tileHeight * 2
- );
-
- uiLabelInit(
- &UICUTSCENE.itemLabel,
- UICUTSCENE.itemText,
- UICUTSCENE.itemSprites, UI_CUTSCENE_SPRITES_MAX
- );
- uiLabelSetX(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanX);
- uiLabelSetY(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanY +
- (float_t)FONT_DEFAULT.tileset->tileHeight * 3
- );
-
- errorOk();
-}
-
-errorret_t uiCutsceneDraw() {
- const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
- if(!item || !CUTSCENE_SYSTEM.scene) {
- stringCopy(
- UICUTSCENE.countText, "Cutscene Inactive", UI_CUTSCENE_TEXT_MAX - 1
- );
- UICUTSCENE.itemText[0] = '\0';
- } else {
- const char_t *name = CUTSCENE_ITEM_CALLBACKS[item->type].jsonName;
- if(!name) name = "???";
-
- stringFormat(
- UICUTSCENE.countText,
- UI_CUTSCENE_TEXT_MAX - 1,
- "Cutscene Item Count: %d",
- (int_t)CUTSCENE_SYSTEM.scene->itemCount
- );
- stringFormat(
- UICUTSCENE.itemText,
- UI_CUTSCENE_TEXT_MAX - 1,
- "Current Item: %s",
- name
- );
- }
- UICUTSCENE.countLabel.dirty = true;
- UICUTSCENE.itemLabel.dirty = true;
-
- errorChain(uiLabelRender(&UICUTSCENE.countLabel, COLOR_GREEN));
- errorChain(uiLabelRender(&UICUTSCENE.itemLabel, COLOR_GREEN));
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/debug/uicutscene.h b/src/dusk/ui/debug/uicutscene.h
deleted file mode 100644
index 07fa9441..00000000
--- a/src/dusk/ui/debug/uicutscene.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-
-#define UI_CUTSCENE_TEXT_MAX 64
-#define UI_CUTSCENE_SPRITES_MAX UI_CUTSCENE_TEXT_MAX
-
-typedef struct {
- uilabel_t countLabel;
- char_t countText[UI_CUTSCENE_TEXT_MAX];
- spritebatchsprite_t countSprites[UI_CUTSCENE_SPRITES_MAX];
-
- uilabel_t itemLabel;
- char_t itemText[UI_CUTSCENE_TEXT_MAX];
- spritebatchsprite_t itemSprites[UI_CUTSCENE_SPRITES_MAX];
-} uicutscene_t;
-
-extern uicutscene_t UICUTSCENE;
-
-/**
- * Initializes the cutscene debug labels.
- *
- * @return Any error that occurs.
- */
-errorret_t uiCutsceneInit();
-
-/**
- * Draws the currently running cutscene's item count and current item
- * type as two lines ("Item Count: %d" / "Current Item: %s"), or a
- * single "Cutscene Inactive" line if no cutscene is running. Renders
- * below UIPLAYERPOS (see ui/debug/uiplayerpos.h).
- *
- * @return Any error that occurs.
- */
-errorret_t uiCutsceneDraw();
diff --git a/src/dusk/ui/debug/uifps.c b/src/dusk/ui/debug/uifps.c
deleted file mode 100644
index d5df8ac5..00000000
--- a/src/dusk/ui/debug/uifps.c
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uifps.h"
-#include "time/time.h"
-#include "util/string.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/color.h"
-#include "display/screen/screen.h"
-#include "engine/engine.h"
-
-uifps_t UIFPS;
-
-errorret_t uiFPSInit() {
- uiLabelInit(
- &UIFPS.fpsLabel,
- UIFPS.fpsText,
- UIFPS.fpsSprites, UI_FPS_SPRITES_MAX
- );
- uiLabelSetX(&UIFPS.fpsLabel, (float_t)SCREEN.scanX);
- uiLabelSetY(&UIFPS.fpsLabel, (float_t)SCREEN.scanY);
-
- uiLabelInit(
- &UIFPS.versionLabel,
- UIFPS.versionText,
- UIFPS.versionSprites, UI_FPS_VERSION_SPRITES_MAX
- );
- stringCopy(UIFPS.versionText, ENGINE.version, UI_FPS_VERSION_TEXT_MAX - 1);
- UIFPS.versionLabel.dirty = true;
- uiLabelRebuffer(&UIFPS.versionLabel);
- uiLabelSetX(&UIFPS.versionLabel, (float_t)(
- SCREEN.scanX + SCREEN.scanWidth - UIFPS.versionLabel.width
- ));
- uiLabelSetY(&UIFPS.versionLabel, (float_t)(
- SCREEN.scanY + SCREEN.scanHeight - UIFPS.versionLabel.height
- ));
-
- errorOk();
-}
-
-errorret_t uiFPSDraw() {
- // Get now.
- dusktimeepoch_t now = timeGetEpoch();
- double_t delta = now.time - UIFPS.lastTick.time;
- UIFPS.lastTick = now;
-
- // Raw current FPS
- float_t fps = delta > 0 ? 1.0 / delta : 0.0;
-
- // Average FPS using exponential moving average
- const float_t alpha = 0.1f; // Smoothing factor
- if(UIFPS.fpsAverage == 0.0f) {
- UIFPS.fpsAverage = fps; // Initialize average on first run
- } else {
- UIFPS.fpsAverage = alpha * fps + (1.0f - alpha) * UIFPS.fpsAverage;
- }
-
- stringFormat(
- UIFPS.fpsText,
- UI_FPS_TEXT_MAX - 1,
- "%.1f/%.1fms",
- UIFPS.fpsAverage,
- delta * 1000.0f
- );
- UIFPS.fpsLabel.dirty = true;
-
- color_t textColor;
- if(fps >= 55.0f) {
- textColor = COLOR_GREEN;
- } else if(fps >= 45.0f) {
- textColor = COLOR_YELLOW;
- } else {
- textColor = COLOR_RED;
- }
-
- errorChain(uiLabelRender(&UIFPS.fpsLabel, textColor));
- errorChain(spriteBatchFlush());
-
- errorChain(uiLabelRender(&UIFPS.versionLabel, color(255, 255, 255, 128)));
-
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/debug/uifps.h b/src/dusk/ui/debug/uifps.h
deleted file mode 100644
index fda1345b..00000000
--- a/src/dusk/ui/debug/uifps.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "time/timeepoch.h"
-#include "ui/widget/uilabel.h"
-
-#define UI_FPS_TEXT_MAX 32
-#define UI_FPS_SPRITES_MAX UI_FPS_TEXT_MAX
-#define UI_FPS_VERSION_TEXT_MAX 32
-#define UI_FPS_VERSION_SPRITES_MAX UI_FPS_VERSION_TEXT_MAX
-
-typedef struct {
- dusktimeepoch_t lastTick;
- float_t fpsAverage;
-
- uilabel_t fpsLabel;
- char_t fpsText[UI_FPS_TEXT_MAX];
- spritebatchsprite_t fpsSprites[UI_FPS_SPRITES_MAX];
-
- uilabel_t versionLabel;
- char_t versionText[UI_FPS_VERSION_TEXT_MAX];
- spritebatchsprite_t versionSprites[UI_FPS_VERSION_SPRITES_MAX];
-} uifps_t;
-
-extern uifps_t UIFPS;
-
-/**
- * Initializes the FPS counter's labels.
- *
- * @return Any error that occurs.
- */
-errorret_t uiFPSInit();
-
-/**
- * Draws the FPS counter on the screen, and also does the update (for now).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFPSDraw();
\ No newline at end of file
diff --git a/src/dusk/ui/debug/uiplayerpos.c b/src/dusk/ui/debug/uiplayerpos.c
deleted file mode 100644
index ce7972c2..00000000
--- a/src/dusk/ui/debug/uiplayerpos.c
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiplayerpos.h"
-#include "util/string.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/spritebatch/spritebatch.h"
-#include "rpg/entity/entity.h"
-#include "rpg/entity/entitytype.h"
-#include "rpg/overworld/worldpos.h"
-#include "rpg/overworld/map.h"
-
-uiplayerpos_t UIPLAYERPOS;
-
-errorret_t uiPlayerPosInit() {
- uiLabelInit(
- &UIPLAYERPOS.label,
- UIPLAYERPOS.text,
- UIPLAYERPOS.sprites, UI_PLAYERPOS_SPRITES_MAX
- );
- uiLabelSetX(&UIPLAYERPOS.label, (float_t)SCREEN.scanX);
- uiLabelSetY(&UIPLAYERPOS.label, (float_t)SCREEN.scanY +
- (float_t)FONT_DEFAULT.tileset->tileHeight
- );
-
- errorOk();
-}
-
-errorret_t uiPlayerPosDraw() {
- entity_t *player = NULL;
- for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
- if(ENTITIES[i].type == ENTITY_TYPE_PLAYER) {
- player = &ENTITIES[i];
- break;
- }
- }
- if(!player) errorOk();
-
- chunkpos_t chunkPos;
- worldPosToChunkPos(&player->position, &chunkPos);
-
- stringFormat(
- UIPLAYERPOS.text,
- UI_PLAYERPOS_TEXT_MAX - 1,
- "%s %d,%d,%d[%d,%d,%d]#%d",
- MAP.name,
- (int_t)player->position.x,
- (int_t)player->position.y,
- (int_t)player->position.z,
- (int_t)chunkPos.x,
- (int_t)chunkPos.y,
- (int_t)chunkPos.z,
- (int_t)player->chunkIndex
- );
- UIPLAYERPOS.label.dirty = true;
-
- errorChain(uiLabelRender(&UIPLAYERPOS.label, COLOR_GREEN));
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/debug/uiplayerpos.h b/src/dusk/ui/debug/uiplayerpos.h
deleted file mode 100644
index 1fb03ff7..00000000
--- a/src/dusk/ui/debug/uiplayerpos.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-
-#define UI_PLAYERPOS_TEXT_MAX 64
-#define UI_PLAYERPOS_SPRITES_MAX UI_PLAYERPOS_TEXT_MAX
-
-typedef struct {
- uilabel_t label;
- char_t text[UI_PLAYERPOS_TEXT_MAX];
- spritebatchsprite_t sprites[UI_PLAYERPOS_SPRITES_MAX];
-} uiplayerpos_t;
-
-extern uiplayerpos_t UIPLAYERPOS;
-
-/**
- * Initializes the player position label.
- *
- * @return Any error that occurs.
- */
-errorret_t uiPlayerPosInit();
-
-/**
- * Draws the current map name, player world position, chunk position, and
- * chunk index on screen. Searches for the first ENTITY_TYPE_PLAYER and
- * renders: MAPNAME WORLDX,WORLDY,WORLDZ[chunkx,chunky,chunkz]#chunkIndex
- *
- * @return Any error that occurs.
- */
-errorret_t uiPlayerPosDraw();
diff --git a/src/dusk/ui/dialog/CMakeLists.txt b/src/dusk/ui/dialog/CMakeLists.txt
deleted file mode 100644
index bfaab3ef..00000000
--- a/src/dusk/ui/dialog/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uiconfirm.c
-)
-
-add_subdirectory(save)
-add_subdirectory(keyboard)
\ No newline at end of file
diff --git a/src/dusk/ui/dialog/keyboard/CMakeLists.txt b/src/dusk/ui/dialog/keyboard/CMakeLists.txt
deleted file mode 100644
index 9e5f2567..00000000
--- a/src/dusk/ui/dialog/keyboard/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uikeyboard.c
-)
diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c
deleted file mode 100644
index 8d56f69b..00000000
--- a/src/dusk/ui/dialog/keyboard/uikeyboard.c
+++ /dev/null
@@ -1,610 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uikeyboard.h"
-#include "ui/widget/uiframe.h"
-#include "ui/widget/uibutton.h"
-#include "ui/dialog/uiconfirm.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "util/math.h"
-#include "display/screen/screen.h"
-#include "display/color.h"
-#include "display/text/font.h"
-#include "display/spritebatch/spritebatch.h"
-#include "time/time.h"
-
-// Key cell size, expressed in font tiles so it scales if the font ever
-// does. Row height is derived from this ratio in uiKeyboardDraw rather
-// than being its own fixed constant, so a key cell keeps its proportions
-// even when contentWidth is stretched wider by a long title/text label.
-#define UI_KEYBOARD_KEY_WIDTH ((float_t)(FONT_DEFAULT_TILE_WIDTH * 4))
-#define UI_KEYBOARD_KEY_HEIGHT ((float_t)(FONT_DEFAULT_TILE_HEIGHT * 2))
-
-uikeyboard_t UI_KEYBOARD;
-
-// String literals have static storage duration, so these stay valid for
-// as long as the process runs - safe for uiButtonInit's non-copying
-// label contract. "" cells are blank filler/reserved slots, NULL is
-// shorthand for the same thing where there's no key at all - see
-// uikeyboard.h and uiKeyboardNormalizeKey.
-//
-// Unshifted, caps off - also the canonical table (see
-// uiKeyboardGetCanonicalKey).
-static const char_t *const UI_KEYBOARD_KEYS_NONE[UI_KEYBOARD_KEY_COUNT] = {
- // 1 2 3 4 5 6 7 8 9 0 DEL
- "1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
- UI_KEYBOARD_KEY_BACKSPACE,
-
- // q w e r t y u i o p
- "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", NULL,
-
- // CAPS a s d f g h j k l (NEWLINE)
- UI_KEYBOARD_KEY_CAPS,
- "a", "s", "d", "f", "g", "h", "j", "k", "l", NULL,
-
- // SHIFT z x c v b n m SHIFT
- UI_KEYBOARD_KEY_SHIFT,
- NULL,
- "z", "x", "c", "v", "b", "n", "m",
- NULL,
- UI_KEYBOARD_KEY_SHIFT,
-
- // (CANCEL) SPACE (CONFIRM)
- NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
- NULL
-};
-
-// Caps on - letters uppercase; caps doesn't affect digits/symbols on a
-// physical keyboard, so this table is otherwise identical to _NONE.
-static const char_t *const UI_KEYBOARD_KEYS_CAPS[UI_KEYBOARD_KEY_COUNT] = {
- "1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
- UI_KEYBOARD_KEY_BACKSPACE,
-
- "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
-
- UI_KEYBOARD_KEY_CAPS,
- "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
-
- UI_KEYBOARD_KEY_SHIFT,
- NULL,
- "Z", "X", "C", "V", "B", "N", "M",
- NULL,
- UI_KEYBOARD_KEY_SHIFT,
-
- NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
- NULL
-};
-
-// Shift held - letters uppercase, digits become their US-layout shift
-// symbols, '-'/'=' become '_'/'+'. Takes priority over caps (see
-// uiKeyboardGetChar).
-static const char_t *const UI_KEYBOARD_KEYS_SHIFT[UI_KEYBOARD_KEY_COUNT] = {
- "!", "@", "#", "$", "%", "^", "&", "*", "(", ")",
- UI_KEYBOARD_KEY_BACKSPACE,
-
- "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
-
- UI_KEYBOARD_KEY_CAPS,
- "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
-
- UI_KEYBOARD_KEY_SHIFT,
- NULL,
- "Z", "X", "C", "V", "B", "N", "M",
- NULL,
- UI_KEYBOARD_KEY_SHIFT,
-
- NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
- NULL
-};
-
-// Collapses a table entry's NULL ("no key here") down to "" (blank
-// filler), the only form of "nothing" callers need to handle.
-const char_t * uiKeyboardNormalizeKey(const char_t *key) {
- return key != NULL ? key : "";
-}
-
-// Shift takes priority over caps - there's no fourth table for "shift
-// while caps is on", unlike a physical keyboard.
-const char_t *const * uiKeyboardSelectTable(
- const bool_t shift,
- const bool_t caps
-) {
- if(shift) return UI_KEYBOARD_KEYS_SHIFT;
- if(caps) return UI_KEYBOARD_KEYS_CAPS;
- return UI_KEYBOARD_KEYS_NONE;
-}
-
-uint8_t uiKeyboardBuildItems(
- uimenuitem_t *items,
- const bool_t shift,
- const bool_t caps
-) {
- assertNotNull(items, "Items cannot be NULL");
- const char_t *const *table = uiKeyboardSelectTable(shift, caps);
-
- for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
- items[i].type = UI_MENU_WIDGET_TYPE_BUTTON;
- uiButtonInit(&items[i].button, uiKeyboardNormalizeKey(table[i]));
- }
-
- return UI_KEYBOARD_KEY_COUNT;
-}
-
-void uiKeyboardUpdateDisplay(
- uimenuitem_t *items,
- const bool_t shift,
- const bool_t caps
-) {
- assertNotNull(items, "Items cannot be NULL");
- const char_t *const *table = uiKeyboardSelectTable(shift, caps);
-
- for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
- // These reserved slots are blank in every shift/caps table - their
- // real labels (NEWLINE/CANCEL/CONFIRM) are patched in once at open
- // time and aren't affected by shift/caps, so leave them alone here.
- if(
- i == UI_KEYBOARD_NEWLINE_INDEX ||
- i == UI_KEYBOARD_CANCEL_INDEX ||
- i == UI_KEYBOARD_CONFIRM_INDEX
- ) continue;
-
- items[i].button.label.text = uiKeyboardNormalizeKey(table[i]);
- items[i].button.label.dirty = true;
-
- const char_t *canonical = uiKeyboardGetCanonicalKey(i);
- if(stringCompare(canonical, UI_KEYBOARD_KEY_CAPS) == 0) {
- uiButtonSetActive(&items[i].button, caps);
- } else if(stringCompare(canonical, UI_KEYBOARD_KEY_SHIFT) == 0) {
- uiButtonSetActive(&items[i].button, shift);
- }
- }
-}
-
-const char_t * uiKeyboardGetCanonicalKey(const uint8_t index) {
- assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
- return uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]);
-}
-
-bool_t uiKeyboardIsShiftSensitive(const uint8_t index) {
- assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
- return stringCompare(
- uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]),
- uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_SHIFT[index])
- ) != 0;
-}
-
-char_t uiKeyboardGetChar(
- const uint8_t index,
- const bool_t shift,
- const bool_t caps
-) {
- assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
- const char_t *const *table = uiKeyboardSelectTable(shift, caps);
- return uiKeyboardNormalizeKey(table[index])[0];
-}
-
-void uiKeyboardFocusConfirm(void) {
- uiMenuSetPosition(
- &UI_KEYBOARD.menu,
- UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS,
- UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS
- );
-}
-
-bool_t uiKeyboardTextIsBlank(void) {
- for(const char_t *c = UI_KEYBOARD.text; *c != '\0'; c++) {
- if(!stringIsWhitespace(*c)) return false;
- }
- return true;
-}
-
-void uiKeyboardAppendChar(const char_t c) {
- size_t length = strlen(UI_KEYBOARD.text);
-
- // Full - drop the last character instead of ignoring the keypress, so
- // the new one overrides it. Falls through to the normal append below,
- // which now has room again.
- if(length >= UI_KEYBOARD.maxLength) {
- length--;
- UI_KEYBOARD.text[length] = '\0';
- }
-
- if(c == '\n') {
- uint8_t lines = 1;
- for(size_t i = 0; i < length; i++) {
- if(UI_KEYBOARD.text[i] == '\n') lines++;
- }
- if(lines >= UI_KEYBOARD.lineCount) return;
- }
-
- UI_KEYBOARD.text[length] = c;
- UI_KEYBOARD.text[length + 1] = '\0';
- UI_KEYBOARD.textLabel.dirty = true;
- uiLabelRebuffer(&UI_KEYBOARD.textLabel);
-
- // Nothing more can be typed - jump focus to confirm so accepting is a
- // single button press away.
- if(length + 1 >= UI_KEYBOARD.maxLength) uiKeyboardFocusConfirm();
-}
-
-void uiKeyboardBackspace(void) {
- size_t length = strlen(UI_KEYBOARD.text);
- if(length == 0) return;
-
- UI_KEYBOARD.text[length - 1] = '\0';
- UI_KEYBOARD.textLabel.dirty = true;
- uiLabelRebuffer(&UI_KEYBOARD.textLabel);
-}
-
-void uiKeyboardMenuClosed(const uimenu_t *menu) {
- if(UI_KEYBOARD.onInput != NULL) {
- UI_KEYBOARD.onInput(
- UI_KEYBOARD.text, UI_KEYBOARD.confirmed, UI_KEYBOARD.user
- );
- }
-}
-
-bool_t uiKeyboardMenuCancel(const uimenu_t *menu) {
- if(strlen(UI_KEYBOARD.text) > 0) {
- uiKeyboardBackspace();
- return true;
- }
-
- // Nothing left to delete - close, but only if this dialog is even
- // allowed to be cancelled. Closed explicitly (rather than returning
- // false to fall through to uiFocusUpdate's default pop) since that
- // path only pops the focus stack - it never clears UI_KEYBOARD.open,
- // which would leave uiKeyboardDraw drawing a focus-less dialog forever
- // and the next uiKeyboardOpen() tripping its already-open assert.
- if(!UI_KEYBOARD.cancel) return true;
-
- uiKeyboardDoCancel();
- return true;
-}
-
-void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user) {
- if(!result) return;
-
- UI_KEYBOARD.confirmed = true;
- uiKeyboardClose();
-}
-
-void uiKeyboardDoCancel(void) {
- if(UI_KEYBOARD.cancelConfirm) {
- uiConfirmOpen(
- UI_KEYBOARD.cancelConfirmQuestion,
- uiKeyboardCancelConfirmSecondaryResult, NULL
- );
- return;
- }
-
- UI_KEYBOARD.confirmed = false;
- uiKeyboardClose();
-}
-
-void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user) {
- if(!result) return;
-
- UI_KEYBOARD.confirmed = false;
- uiKeyboardClose();
-}
-
-void uiKeyboardMenuSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- if(index == UI_KEYBOARD_CONFIRM_INDEX) {
- if(UI_KEYBOARD.trimmed) {
- stringTrim(UI_KEYBOARD.text);
- UI_KEYBOARD.textLabel.dirty = true;
- uiLabelRebuffer(&UI_KEYBOARD.textLabel);
- }
- if(!UI_KEYBOARD.allowBlank && uiKeyboardTextIsBlank()) return;
-
- if(UI_KEYBOARD.confirm) {
- uiConfirmOpen(
- UI_KEYBOARD.confirmQuestion, uiKeyboardConfirmSecondaryResult, NULL
- );
- return;
- }
-
- UI_KEYBOARD.confirmed = true;
- uiKeyboardClose();
- return;
- }
-
- if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) {
- uiKeyboardDoCancel();
- return;
- }
-
- if(index == UI_KEYBOARD_NEWLINE_INDEX && UI_KEYBOARD.lineCount > 1) {
- if(
- UI_KEYBOARD.onKeyPress != NULL &&
- !UI_KEYBOARD.onKeyPress('\n', UI_KEYBOARD.user)
- ) return;
- uiKeyboardAppendChar('\n');
- return;
- }
-
- // The canonical (unshifted) key, independent of whatever character
- // it's currently drawn as - see uiKeyboardGetCanonicalKey. Used to
- // identify the key, not item->button.label.text, which changes with
- // shift/caps.
- const char_t *key = uiKeyboardGetCanonicalKey(index);
-
- // Blank filler/reserved cell (includes an unused cancel/newline slot).
- if(key[0] == '\0') return;
-
- if(stringCompare(key, UI_KEYBOARD_KEY_BACKSPACE) == 0) {
- uiKeyboardBackspace();
- return;
- }
-
- if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) {
- UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
- uiKeyboardUpdateDisplay(
- UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
- );
- return;
- }
-
- if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) {
- UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
- uiKeyboardUpdateDisplay(
- UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
- );
- return;
- }
-
- char_t c;
- if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) {
- c = ' ';
- } else {
- c = uiKeyboardGetChar(index, UI_KEYBOARD.shift, UI_KEYBOARD.caps);
-
- // Shift is a one-shot, consumed by any letter/digit/-/= it actually
- // affected (and the display refreshed to drop back to whatever caps
- // alone says); caps is a persistent toggle.
- if(UI_KEYBOARD.shift && uiKeyboardIsShiftSensitive(index)) {
- UI_KEYBOARD.shift = false;
- uiKeyboardUpdateDisplay(
- UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
- );
- }
- }
-
- if(
- UI_KEYBOARD.onKeyPress != NULL &&
- !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user)
- ) {
- return;
- }
- uiKeyboardAppendChar(c);
-}
-
-errorret_t uiKeyboardInit(void) {
- memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t));
-
- uiLabelInit(
- &UI_KEYBOARD.titleLabel, UI_KEYBOARD.titleText,
- UI_KEYBOARD.titleSprites, UI_KEYBOARD_TITLE_SPRITES_MAX
- );
- uiLabelInit(
- &UI_KEYBOARD.textLabel, UI_KEYBOARD.text,
- UI_KEYBOARD.textSprites, UI_KEYBOARD_TEXT_SPRITES_MAX
- );
- uiLabelInit(
- &UI_KEYBOARD.cursorLabel, UI_KEYBOARD_CURSOR_TEXT,
- UI_KEYBOARD.cursorSprites, UI_KEYBOARD_CURSOR_SPRITES_MAX
- );
- UI_KEYBOARD.cursorLabel.dirty = true;
-
- errorOk();
-}
-
-bool_t uiKeyboardIsOpen(void) {
- return UI_KEYBOARD.open;
-}
-
-void uiKeyboardOpen(const uikeyboardopen_t *open) {
- assertNotNull(open, "Open parameters cannot be NULL");
- assertFalse(UI_KEYBOARD.open, "Keyboard is already open");
- assertTrue(
- open->maxLength <= UI_KEYBOARD_TEXT_MAX - 1,
- "maxLength exceeds UI_KEYBOARD_TEXT_MAX"
- );
- assertTrue(
- open->lineCount <= UI_KEYBOARD_LINE_COUNT_MAX,
- "lineCount exceeds UI_KEYBOARD_LINE_COUNT_MAX"
- );
-
- stringCopy(
- UI_KEYBOARD.titleText,
- open->title != NULL ? open->title : UI_KEYBOARD_TITLE_DEFAULT,
- UI_KEYBOARD_TITLE_TEXT_MAX - 1
- );
- UI_KEYBOARD.titleLabel.dirty = true;
- uiLabelRebuffer(&UI_KEYBOARD.titleLabel);
-
- UI_KEYBOARD.text[0] = '\0';
- UI_KEYBOARD.textLabel.dirty = true;
- uiLabelRebuffer(&UI_KEYBOARD.textLabel);
-
- UI_KEYBOARD.onInput = open->onInput;
- UI_KEYBOARD.onKeyPress = open->onKeyPress;
- UI_KEYBOARD.cancel = open->cancel;
- UI_KEYBOARD.maxLength = open->maxLength == 0 ?
- UI_KEYBOARD_TEXT_MAX - 1 : open->maxLength;
- UI_KEYBOARD.lineCount = open->lineCount == 0 ? 1 : open->lineCount;
- UI_KEYBOARD.confirm = open->confirm;
- stringCopy(
- UI_KEYBOARD.confirmQuestion,
- open->confirmLabel != NULL ?
- open->confirmLabel : UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT,
- UI_KEYBOARD_CONFIRM_QUESTION_MAX - 1
- );
- UI_KEYBOARD.trimmed = open->trimmed;
- UI_KEYBOARD.allowBlank = open->allowBlank;
- UI_KEYBOARD.user = open->user;
- UI_KEYBOARD.confirmed = false;
- UI_KEYBOARD.caps = false;
- UI_KEYBOARD.shift = false;
- UI_KEYBOARD.open = true;
-
- // Builds the full fixed grid, including blank filler at the
- // newline/confirm/cancel reserved slots - patched with real buttons
- // below where applicable. Letters start lowercase and digits
- // unshifted, matching caps/shift both being reset to false above.
- uiKeyboardBuildItems(UI_KEYBOARD.items, false, false);
-
- if(UI_KEYBOARD.lineCount > 1) {
- uiButtonInit(
- &UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button,
- UI_KEYBOARD_KEY_NEWLINE
- );
- }
-
- uiButtonInit(
- &UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button,
- UI_KEYBOARD_CONFIRM_LABEL
- );
-
- if(UI_KEYBOARD.cancel) {
- uiButtonInit(
- &UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button,
- UI_KEYBOARD_CANCEL_LABEL
- );
- }
-
- uiMenuInit(
- &UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
- );
- uiMenuSetItems(
- &UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT,
- UI_KEYBOARD_COLUMNS
- );
-
- // Back deletes a character rather than closing the dialog outright -
- // see uiKeyboardMenuCancel.
- uiMenuSetCancelCallback(&UI_KEYBOARD.menu, uiKeyboardMenuCancel);
- uiMenuOpen(&UI_KEYBOARD.menu);
-}
-
-void uiKeyboardClose(void) {
- if(!UI_KEYBOARD.open) return;
- UI_KEYBOARD.open = false;
-
- if(uiMenuIsActive(&UI_KEYBOARD.menu)) {
- uiMenuClose(&UI_KEYBOARD.menu);
- } else {
- uiKeyboardMenuClosed(NULL);
- }
-}
-
-errorret_t uiKeyboardDraw(void) {
- if(!UI_KEYBOARD.open) errorOk();
-
- float_t x = (float_t)SCREEN.scanX;
- float_t y = (float_t)SCREEN.scanY;
- float_t width = (float_t)SCREEN.scanWidth;
- float_t height = (float_t)SCREEN.scanHeight;
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- // When multi-line, reserve the full lineCount rows up front so the
- // dialog doesn't resize as newlines are typed - otherwise fall back to
- // the label's own measured height (floored at one row for an empty
- // string, which measures to zero).
- float_t textHeight = UI_KEYBOARD.lineCount > 1 ?
- (float_t)UI_KEYBOARD.lineCount * rowHeight :
- mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
- uint8_t columns = UI_KEYBOARD.menu.columns;
- uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns;
-
- float_t contentWidth = mathMax(
- mathMax(
- (float_t)UI_KEYBOARD.titleLabel.width,
- (float_t)UI_KEYBOARD.textLabel.width
- ),
- (float_t)columns * UI_KEYBOARD_KEY_WIDTH
- );
- float_t colStep = contentWidth / (float_t)columns;
- float_t keyRowHeight =
- colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH);
- float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y
- + textHeight + UI_FRAME_PADDING_Y
- + ((float_t)rows * keyRowHeight);
-
- // The frame now fills the whole screen, but the title/text/keys still
- // lay out as one content block - center that block within the screen.
- float_t contentX = x + (width - contentWidth) * 0.5f;
- float_t contentY = y + (height - contentHeight) * 0.5f;
-
- uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
- uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
- errorChain(uiLabelRender(&UI_KEYBOARD.titleLabel, COLOR_WHITE));
-
- float_t textY = contentY + rowHeight + UI_FRAME_PADDING_Y;
- uiLabelSetX(&UI_KEYBOARD.textLabel, contentX);
- uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
- errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE));
-
- // Insertion point is always the end of text - there's no mid-string
- // cursor movement - so find it by counting characters since the last
- // newline (column) and newlines overall (line). Once maxLength is
- // reached, a new key overrides the last character (see
- // uiKeyboardAppendChar) rather than appending one, so stop one
- // character short here too - the cursor should sit on that last
- // character, not past it, to reflect what typing will actually do.
- size_t textLength = strlen(UI_KEYBOARD.text);
- size_t cursorLimit = textLength >= UI_KEYBOARD.maxLength ?
- textLength - 1 : textLength;
-
- uint8_t cursorLine = 0;
- uint8_t cursorCol = 0;
- for(size_t i = 0; i < cursorLimit; i++) {
- if(UI_KEYBOARD.text[i] == '\n') { cursorLine++; cursorCol = 0; }
- else cursorCol++;
- }
-
- if(mathModFloat(TIME.time, UI_KEYBOARD_CURSOR_BLINK_RATE * 2.0f) <
- UI_KEYBOARD_CURSOR_BLINK_RATE
- ) {
- uiLabelSetX(
- &UI_KEYBOARD.cursorLabel,
- contentX + (float_t)cursorCol * (float_t)FONT_DEFAULT_TILE_WIDTH
- );
- uiLabelSetY(
- &UI_KEYBOARD.cursorLabel, textY + (float_t)cursorLine * rowHeight
- );
- errorChain(uiLabelRender(&UI_KEYBOARD.cursorLabel, COLOR_WHITE));
- }
-
- // Drawn directly rather than via uiMenuDraw - that helper hardcodes row
- // spacing to the font's native tile height, which would keep the grid
- // vertically compact no matter how much contentWidth grows. Focus
- // highlighting is unaffected: it's applied to item->button.highlighted
- // by the focus system independently of whoever calls uiButtonDraw.
- float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y;
- for(uint8_t i = 0; i < UI_KEYBOARD.menu.itemCount; i++) {
- float_t ix = contentX + (float_t)(i % columns) * colStep;
- float_t iy = menuY + (float_t)(i / columns) * keyRowHeight;
- errorChain(uiButtonDraw(&UI_KEYBOARD.items[i].button, ix, iy));
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-errorret_t uiKeyboardDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.h b/src/dusk/ui/dialog/keyboard/uikeyboard.h
deleted file mode 100644
index 565d7cba..00000000
--- a/src/dusk/ui/dialog/keyboard/uikeyboard.h
+++ /dev/null
@@ -1,490 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-#include "ui/widget/uimenu.h"
-#include "display/text/font.h"
-
-// A fixed 11x5 QWERTY grid - the only layout that exists (numbers/
-// symbols layouts were removed as separate concepts; digits already
-// live on this one):
-// 1 2 3 4 5 6 7 8 9 0 DEL
-// q w e r t y u i o p
-// CAPS a s d f g h j k l (NEWLINE)
-// SHIFT z x c v b n m SHIFT
-// (CANCEL) SPACE (CONFIRM)
-// Rows shorter than 11 columns are padded with blank filler buttons
-// (empty label) so every key lands in its intended position - the menu/
-// focus grid only supports uniform columns, it has no concept of a key
-// spanning multiple cells or a row being narrower than the grid, so this
-// is the only way to get real positioning out of it. A filler cell is
-// still a focusable/navigable-to grid slot, it just does nothing when
-// selected - see uiKeyboardMenuSelected's blank-label check. A
-// UI_KEYBOARD_KEYS_* table may use NULL for a slot with no key at all -
-// see uiKeyboardGetCanonicalKey, which normalizes it to the same blank
-// filler behavior.
-//
-// NEWLINE/CANCEL/CONFIRM are reserved slots the tables always leave
-// blank - uiKeyboardOpen patches them with real buttons depending on
-// lineCount/cancel (CONFIRM is unconditional, just needed as a fixed
-// slot addressable by constant).
-#define UI_KEYBOARD_COLUMNS 11
-#define UI_KEYBOARD_ROWS 5
-#define UI_KEYBOARD_KEY_COUNT (UI_KEYBOARD_COLUMNS * UI_KEYBOARD_ROWS)
-
-// Reserved slot indices uiKeyboardOpen patches with real buttons.
-#define UI_KEYBOARD_NEWLINE_INDEX 32
-#define UI_KEYBOARD_CANCEL_INDEX 44
-#define UI_KEYBOARD_CONFIRM_INDEX 54
-
-// Labels uiKeyboardMenuSelected checks for by pointer/string compare to
-// tell special keys apart from an ordinary single-character key. A blank
-// ("") label is a filler cell - see above. Identical across every
-// UI_KEYBOARD_KEYS_* table, since these don't change with shift/caps.
-#define UI_KEYBOARD_KEY_SPACE FONT_ICON_SPACE
-#define UI_KEYBOARD_KEY_BACKSPACE FONT_ICON_BACKSPACE
-#define UI_KEYBOARD_KEY_SHIFT FONT_ICON_SHIFT
-#define UI_KEYBOARD_KEY_CAPS FONT_ICON_CAPSLOCK
-// Only patched into UI_KEYBOARD_NEWLINE_INDEX when
-// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank.
-#define UI_KEYBOARD_KEY_NEWLINE FONT_ICON_NEWLINE
-
-#define UI_KEYBOARD_TITLE_TEXT_MAX 64
-#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
-#define UI_KEYBOARD_TEXT_MAX 64
-#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
-// The grid is a fixed size - only one layout exists today, so this is
-// just its key count.
-#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT
-
-// Hardcoded for now - a uiModalOpen-style title/button text override is
-// planned (see uikeyboardopen_t) but not built yet. Patched into the
-// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than
-// appended, since the grid is a fixed shape.
-#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
-#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
-#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
-
-#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
-// Used when uikeyboardopen_t.confirmLabel is NULL.
-#define UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT "Is this correct?"
-
-#define UI_KEYBOARD_CANCEL_QUESTION_MAX 64
-// Used when uikeyboardopen_t.cancelConfirmLabel is NULL.
-#define UI_KEYBOARD_CANCEL_QUESTION_DEFAULT "Discard your changes?"
-
-// Maximum number of lines uikeyboardopen_t.lineCount may request.
-#define UI_KEYBOARD_LINE_COUNT_MAX 4
-
-// Marks where the next typed character will be inserted (always the end
-// of the current text - there's no mid-string cursor movement).
-#define UI_KEYBOARD_CURSOR_TEXT "_"
-#define UI_KEYBOARD_CURSOR_SPRITES_MAX 1
-// Seconds per on/off half-cycle of the cursor blink.
-#define UI_KEYBOARD_CURSOR_BLINK_RATE 0.5f
-
-/**
- * Callback invoked once the keyboard dialog is dismissed.
- *
- * @param text The entered text. Only meaningful when confirmed is true;
- * points at UI_KEYBOARD's own buffer, so copy it out if it needs to
- * outlive the next uiKeyboardOpen call.
- * @param confirmed True if the confirm button was picked, false if
- * cancelled.
- * @param user Arbitrary pointer passed via uikeyboardopen_t.user.
- */
-typedef void (*uikeyboardinputcallback_t)(
- const char_t *text,
- const bool_t confirmed,
- void *user
-);
-
-/**
- * Callback invoked before a key press is applied, giving the caller a
- * chance to reject it - e.g. an email field rejecting "%".
- *
- * @param c The character about to be entered (SPACE included, DEL/
- * confirm/cancel excluded - those aren't text being entered).
- * @param user Arbitrary pointer passed via uikeyboardopen_t.user.
- * @returns True to allow the character, false to reject it (ignored,
- * nothing is entered).
- */
-typedef bool_t (*uikeyboardkeypresscallback_t)(const char_t c, void *user);
-
-/**
- * Parameters for uiKeyboardOpen. Expect this to grow (e.g. button text
- * overrides) as more dialogs start using the keyboard.
- */
-typedef struct {
- // Title text shown above the entered text; copied internally, safe to
- // be transient. Displayed as-is - resolve any locale string yourself
- // before passing it in. NULL uses UI_KEYBOARD_TITLE_DEFAULT ("ENTER
- // YOUR TEXT").
- const char_t *title;
-
- // Fired once the dialog is dismissed, confirmed or cancelled. May be
- // NULL.
- uikeyboardinputcallback_t onInput;
-
- // Fired for each key press before it's applied. May be NULL to allow
- // everything.
- uikeyboardkeypresscallback_t onKeyPress;
-
- // Whether the cancel button is shown, and whether pressing back with
- // no text entered closes the dialog - see uiKeyboardMenuCancel.
- bool_t cancel;
-
- // Maximum length of the entered text, in characters excluding the
- // null terminator. 0 defaults to the buffer's full capacity
- // (UI_KEYBOARD_TEXT_MAX - 1). Must not exceed that.
- size_t maxLength;
-
- // When true, picking confirm opens a second uiConfirm "are you sure"
- // dialog (see confirmLabel) on top of the keyboard before actually
- // closing it - picking Cancel there returns to the keyboard with the
- // text untouched.
- bool_t confirm;
-
- // Question text/locale message ID for the second confirm dialog when
- // confirm is true; copied internally, safe to be transient. NULL uses
- // UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT. Ignored when confirm is false.
- const char_t *confirmLabel;
-
- // When true, actually cancelling (the CANCEL button, or back with
- // nothing left to delete - see uiKeyboardMenuCancel) opens a second
- // uiConfirm "are you sure" dialog (see cancelConfirmLabel) before the
- // keyboard closes - picking Cancel there returns to the keyboard with
- // the text untouched. Ignored when cancel is false, since there's
- // nothing to confirm.
- bool_t cancelConfirm;
-
- // Question text/locale message ID for the cancel confirm dialog when
- // cancelConfirm is true; copied internally, safe to be transient. NULL
- // uses UI_KEYBOARD_CANCEL_QUESTION_DEFAULT. Ignored when cancelConfirm
- // is false.
- const char_t *cancelConfirmLabel;
-
- // If true, leading/trailing whitespace is trimmed from the entered
- // text when confirm is pressed, before it's checked against
- // allowBlank or passed to onInput.
- bool_t trimmed;
-
- // If true, a blank result is accepted when confirm is pressed - blank
- // means an empty string, or (when trimmed is false) a string of only
- // spaces. When false, pressing confirm does nothing while the text is
- // blank - see uiKeyboardTextIsBlank.
- bool_t allowBlank;
-
- // Maximum number of lines the entered text can span, from 1 to
- // UI_KEYBOARD_LINE_COUNT_MAX. 0 defaults to 1. When > 1, a NEWLINE key
- // is added to the grid, capped at inserting lineCount - 1 newlines.
- uint8_t lineCount;
-
- // Arbitrary pointer passed to onInput/onKeyPress.
- void *user;
-} uikeyboardopen_t;
-
-typedef struct {
- uilabel_t titleLabel;
- char_t titleText[UI_KEYBOARD_TITLE_TEXT_MAX];
- spritebatchsprite_t titleSprites[UI_KEYBOARD_TITLE_SPRITES_MAX];
-
- uilabel_t textLabel;
- char_t text[UI_KEYBOARD_TEXT_MAX];
- spritebatchsprite_t textSprites[UI_KEYBOARD_TEXT_SPRITES_MAX];
-
- // Blinking insertion-point marker, positioned each draw at the end of
- // text - a separate label rather than appending to text/textSprites,
- // since neither buffer has spare room for it (see
- // UI_KEYBOARD_TEXT_MAX/UI_KEYBOARD_TEXT_SPRITES_MAX).
- uilabel_t cursorLabel;
- spritebatchsprite_t cursorSprites[UI_KEYBOARD_CURSOR_SPRITES_MAX];
-
- uimenu_t menu;
- uimenuitem_t items[UI_KEYBOARD_ITEMS_MAX];
-
- char_t confirmQuestion[UI_KEYBOARD_CONFIRM_QUESTION_MAX];
- char_t cancelConfirmQuestion[UI_KEYBOARD_CANCEL_QUESTION_MAX];
-
- uikeyboardinputcallback_t onInput;
- uikeyboardkeypresscallback_t onKeyPress;
- void *user;
- size_t maxLength;
- uint8_t lineCount;
- bool_t cancel;
- bool_t confirm;
- bool_t cancelConfirm;
- bool_t trimmed;
- bool_t allowBlank;
- bool_t open;
- bool_t confirmed;
-
- // Persistent caps-lock toggle, and a one-shot shift consumed by the
- // next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels
- // are redrawn to match (see uiKeyboardUpdateDisplay) rather than
- // staying static.
- bool_t caps;
- bool_t shift;
-} uikeyboard_t;
-
-extern uikeyboard_t UI_KEYBOARD;
-
-/**
- * Initializes the on-screen keyboard.
- *
- * @return Any error that occurs.
- */
-errorret_t uiKeyboardInit(void);
-
-/**
- * Draws the on-screen keyboard: a semi-transparent black backdrop
- * covering the whole screen, then its own centered frame with the title,
- * currently entered text, and the key grid. No-op when not open.
- *
- * @return Any error that occurs.
- */
-errorret_t uiKeyboardDraw(void);
-
-/**
- * Returns true when the keyboard dialog is currently open.
- *
- * @returns True if open.
- */
-bool_t uiKeyboardIsOpen(void);
-
-/**
- * Opens the on-screen keyboard with an empty text buffer.
- *
- * @param open Parameters for this dialog; copied internally, safe to be
- * transient. Cannot be NULL.
- */
-void uiKeyboardOpen(const uikeyboardopen_t *open);
-
-/**
- * Closes the keyboard dialog, invoking the result callback set by
- * uiKeyboardOpen as cancelled (confirmed=false) unless the confirm
- * button was what triggered this close. No-op when already closed.
- */
-void uiKeyboardClose(void);
-
-/**
- * Disposes of the on-screen keyboard.
- *
- * @return Any error that occurs.
- */
-errorret_t uiKeyboardDispose(void);
-
-/**
- * Appends a character to the currently entered text, silently ignoring
- * it once UI_KEYBOARD.maxLength is reached, or (for '\n') once the text
- * already has UI_KEYBOARD.lineCount - 1 newlines. Does not consult
- * onKeyPress - that happens in uiKeyboardMenuSelected before this is
- * called. Moves focus to the confirm button once this append fills the
- * text to maxLength - see uiKeyboardFocusConfirm.
- *
- * @param c The character to append.
- */
-void uiKeyboardAppendChar(const char_t c);
-
-/**
- * Moves the menu's focus to the confirm button.
- */
-void uiKeyboardFocusConfirm(void);
-
-/**
- * Returns whether the currently entered text counts as blank for
- * UI_KEYBOARD.allowBlank purposes - an empty string, or (when
- * UI_KEYBOARD.trimmed is false, so whitespace-only text was never
- * reduced to empty) a string of only whitespace.
- *
- * @returns True if blank.
- */
-bool_t uiKeyboardTextIsBlank(void);
-
-/**
- * Removes the last character of the currently entered text. No-op when
- * already empty.
- */
-void uiKeyboardBackspace(void);
-
-/**
- * Internal menu callback - routes a picked key/confirm/cancel item to
- * uiKeyboardAppendChar/uiKeyboardBackspace/uiKeyboardClose, consulting
- * onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
- * is set, picking confirm opens a second uiConfirm dialog (see
- * uiKeyboardConfirmSecondaryResult) instead of closing immediately.
- * SHIFT/CAPS toggle UI_KEYBOARD.shift/caps instead of entering anything.
- * A blank ("") label is a filler/reserved grid cell and is a no-op.
- *
- * @param menu The keyboard's menu.
- * @param index Index of the picked item.
- * @param item The picked item.
- */
-void uiKeyboardMenuSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-);
-
-/**
- * Result callback for the second "are you sure" uiConfirm dialog opened
- * when UI_KEYBOARD.confirm is set - see uiKeyboardMenuSelected.
- *
- * @param result True if Confirm was picked there, false if Cancel.
- * @param user Unused.
- */
-void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user);
-
-/**
- * Actually cancels the keyboard - sets confirmed to false and closes the
- * dialog. Shared by uiKeyboardMenuCancel (back with nothing to delete)
- * and uiKeyboardMenuSelected (the CANCEL button); called directly when
- * UI_KEYBOARD.cancelConfirm is false, or as the result callback (see
- * uiKeyboardCancelConfirmSecondaryResult) once the "are you sure" dialog
- * confirms it when cancelConfirm is true.
- */
-void uiKeyboardDoCancel(void);
-
-/**
- * Result callback for the "are you sure" uiConfirm dialog opened when
- * UI_KEYBOARD.cancelConfirm is set - see uiKeyboardDoCancel.
- *
- * @param result True if Confirm was picked there, false if Cancel.
- * @param user Unused.
- */
-void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user);
-
-/**
- * Internal menu callback - fires the result callback set by
- * uiKeyboardOpen once the menu finishes closing.
- *
- * @param menu Unused; matches uimenuclosedcallback_t.
- */
-void uiKeyboardMenuClosed(const uimenu_t *menu);
-
-/**
- * Internal menu cancel callback (see uiMenuSetCancelCallback): back
- * deletes the last entered character; with nothing left to delete, it
- * closes the dialog as cancelled via uiKeyboardClose, but only when
- * UI_KEYBOARD.cancel allows it - otherwise back is swallowed and does
- * nothing.
- *
- * @param menu The keyboard's menu.
- * @returns Always true - this always fully handles back itself
- * (backspace, explicit close, or swallow), never falling through to the
- * default pop.
- */
-bool_t uiKeyboardMenuCancel(const uimenu_t *menu);
-
-/**
- * Collapses a UI_KEYBOARD_KEYS_* table entry's NULL ("no key here") down
- * to "" (blank filler), the only form of "nothing" callers need to
- * handle.
- *
- * @param key A table entry, possibly NULL.
- * @returns key, or "" if key was NULL.
- */
-const char_t * uiKeyboardNormalizeKey(const char_t *key);
-
-/**
- * Picks which of UI_KEYBOARD_KEYS_NONE/_CAPS/_SHIFT is active for the
- * given state - shift takes priority over caps when both are set.
- *
- * @param shift Current shift state.
- * @param caps Current caps state.
- * @returns The selected table.
- */
-const char_t *const * uiKeyboardSelectTable(
- const bool_t shift,
- const bool_t caps
-);
-
-/**
- * Builds the key grid's buttons into items, starting at index 0, one
- * UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_KEY_COUNT of
- * them, in row-major order across UI_KEYBOARD_COLUMNS columns). Each
- * button's label is a single character, one of the UI_KEYBOARD_KEY_*
- * literals, or "" for a blank filler/reserved slot, read straight from
- * whichever of the three UI_KEYBOARD_KEYS_* tables shift/caps selects
- * (see uiKeyboardGetChar) - can be refreshed later with
- * uiKeyboardUpdateDisplay. See uiKeyboardGetCanonicalKey for identifying
- * a picked key independent of its displayed character.
- *
- * @param items Destination array; must have room for at least
- * UI_KEYBOARD_KEY_COUNT entries.
- * @param shift Initial shift state - see uiKeyboardGetChar.
- * @param caps Initial caps state - see uiKeyboardGetChar.
- * @returns The number of items written (UI_KEYBOARD_KEY_COUNT).
- */
-uint8_t uiKeyboardBuildItems(
- uimenuitem_t *items,
- const bool_t shift,
- const bool_t caps
-);
-
-/**
- * Repoints every key already built into items by uiKeyboardBuildItems at
- * whichever UI_KEYBOARD_KEYS_* table the new shift/caps state selects,
- * marking all of them dirty so they redraw. Unconditional (not just the
- * letter/digit/-/= keys that actually differ between tables) - simpler
- * than tracking which changed, and cheap since this only runs on an
- * infrequent SHIFT/CAPS press.
- *
- * @param items The same array passed to uiKeyboardBuildItems.
- * @param shift New shift state - see uiKeyboardGetChar.
- * @param caps New caps state - see uiKeyboardGetChar.
- */
-void uiKeyboardUpdateDisplay(
- uimenuitem_t *items,
- const bool_t shift,
- const bool_t caps
-);
-
-/**
- * Returns a grid slot's canonical (unshifted, no-caps) key label,
- * independent of whatever character it's currently drawn as - use this
- * to identify which key was picked instead of reading a selected item's
- * own (possibly shifted) label.text.
- *
- * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
- * @returns The slot's canonical label.
- */
-const char_t * uiKeyboardGetCanonicalKey(const uint8_t index);
-
-/**
- * True if a grid slot's character actually differs between the
- * unshifted and shifted tables - i.e. a letter, digit, '-', or '='. False
- * for a slot whose UI_KEYBOARD_KEYS_* entry is the same in every table
- * (SPACE, DEL, SHIFT, CAPS, or blank filler).
- *
- * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
- * @returns True if shift affects this slot's character.
- */
-bool_t uiKeyboardIsShiftSensitive(const uint8_t index);
-
-/**
- * Looks up the character a grid slot represents under the given shift/
- * caps state, straight from whichever UI_KEYBOARD_KEYS_* table they
- * select - shift takes priority over caps when both are set (so, unlike
- * a physical keyboard, shift while caps is on still gives uppercase, not
- * lowercase - there is no fourth table for that combination). For a slot
- * whose label isn't a single character (a blank filler cell, or a
- * multi-character special key like "SPACE"), returns that label's first
- * byte ('\0' for blank).
- *
- * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
- * @param shift Current shift state (one-shot, see UI_KEYBOARD.shift).
- * @param caps Current caps state (persistent toggle, see UI_KEYBOARD.caps).
- * @returns The character to display/type.
- */
-char_t uiKeyboardGetChar(
- const uint8_t index,
- const bool_t shift,
- const bool_t caps
-);
diff --git a/src/dusk/ui/dialog/save/CMakeLists.txt b/src/dusk/ui/dialog/save/CMakeLists.txt
deleted file mode 100644
index aae3928e..00000000
--- a/src/dusk/ui/dialog/save/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uisaveslot.c
- uiselectsave.c
-)
diff --git a/src/dusk/ui/dialog/save/uisaveslot.c b/src/dusk/ui/dialog/save/uisaveslot.c
deleted file mode 100644
index b4e90486..00000000
--- a/src/dusk/ui/dialog/save/uisaveslot.c
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uisaveslot.h"
-#include "ui/widget/uiframe.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/color.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-void uiSaveSlotInit(
- uisaveslot_t *saveSlot,
- const uint8_t slotIndex,
- const char_t *fileName
-) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- assertNotNull(fileName, "File name cannot be NULL");
-
- memoryZero(saveSlot, sizeof(uisaveslot_t));
- saveSlot->slotIndex = slotIndex;
-
- uiLabelInit(
- &saveSlot->numberLabel, saveSlot->numberText,
- saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX
- );
- errorret_t result = localeManagerGetText(
- "ui.save_slot.number_format",
- saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX, 0,
- slotIndex + 1
- );
- if(errorIsNotOk(result)) errorCatch(result);
- saveSlot->numberLabel.dirty = true;
-
- uiLabelInit(
- &saveSlot->nameLabel, saveSlot->nameText,
- saveSlot->nameSprites, UI_SAVE_SLOT_NAME_SPRITES_MAX
- );
- stringCopy(saveSlot->nameText, fileName, UI_SAVE_SLOT_NAME_TEXT_MAX);
- saveSlot->nameLabel.dirty = true;
-}
-
-bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- return saveSlot->highlighted;
-}
-
-void uiSaveSlotSetHighlighted(
- uisaveslot_t *saveSlot, const bool_t highlighted
-) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- saveSlot->highlighted = highlighted;
-}
-
-bool_t uiSaveSlotIsCorrupt(const uisaveslot_t *saveSlot) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- return saveSlot->corrupt;
-}
-
-void uiSaveSlotSetCorrupt(uisaveslot_t *saveSlot, const bool_t corrupt) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- saveSlot->corrupt = corrupt;
-}
-
-uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- return saveSlot->displayType;
-}
-
-void uiSaveSlotSetDisplayType(
- uisaveslot_t *saveSlot,
- const uisaveslotdisplaytype_t displayType
-) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
- saveSlot->displayType = displayType;
-}
-
-errorret_t uiSaveSlotDraw(
- uisaveslot_t *saveSlot,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-) {
- assertNotNull(saveSlot, "Save slot cannot be NULL");
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- const float_t contentX = x + UI_FRAME_START_X;
- const float_t contentY = y + UI_FRAME_START_Y;
-
- color_t color;
- if(saveSlot->corrupt) {
- color = COLOR_RED;
- } else if(saveSlot->highlighted) {
- color = COLOR_RED;
- } else if(saveSlot->displayType == UI_SAVE_SLOT_DISPLAY_TYPE_DELETE) {
- color = COLOR_ORANGE;
- } else {
- color = COLOR_WHITE;
- }
-
- uiLabelSetX(&saveSlot->numberLabel, contentX);
- uiLabelSetY(&saveSlot->numberLabel, contentY);
- errorChain(uiLabelRender(&saveSlot->numberLabel, color));
-
- const float_t nameY = contentY + (float_t)saveSlot->numberLabel.height +
- UI_FRAME_PADDING_Y;
- uiLabelSetX(&saveSlot->nameLabel, contentX);
- uiLabelSetY(&saveSlot->nameLabel, nameY);
- errorChain(uiLabelRender(&saveSlot->nameLabel, color));
-
- errorOk();
-}
diff --git a/src/dusk/ui/dialog/save/uisaveslot.h b/src/dusk/ui/dialog/save/uisaveslot.h
deleted file mode 100644
index 31b7ea43..00000000
--- a/src/dusk/ui/dialog/save/uisaveslot.h
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-
-#define UI_SAVE_SLOT_NUMBER_TEXT_MAX 16
-#define UI_SAVE_SLOT_NUMBER_SPRITES_MAX UI_SAVE_SLOT_NUMBER_TEXT_MAX
-#define UI_SAVE_SLOT_NAME_TEXT_MAX 64
-#define UI_SAVE_SLOT_NAME_SPRITES_MAX UI_SAVE_SLOT_NAME_TEXT_MAX
-
-/**
- * How a save slot entry should be styled: NORMAL for everyday
- * save/load browsing, DELETE to visually flag the row as part of a
- * destructive delete flow.
- */
-typedef enum {
- UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL,
- UI_SAVE_SLOT_DISPLAY_TYPE_DELETE
-} uisaveslotdisplaytype_t;
-
-/**
- * A single framed save slot entry: a slot number and a file name. Later
- * this will grow to show more per-slot save data (level, playtime,
- * etc).
- */
-typedef struct {
- uint8_t slotIndex;
- uisaveslotdisplaytype_t displayType;
-
- uilabel_t numberLabel;
- char_t numberText[UI_SAVE_SLOT_NUMBER_TEXT_MAX];
- spritebatchsprite_t numberSprites[UI_SAVE_SLOT_NUMBER_SPRITES_MAX];
-
- uilabel_t nameLabel;
- char_t nameText[UI_SAVE_SLOT_NAME_TEXT_MAX];
- spritebatchsprite_t nameSprites[UI_SAVE_SLOT_NAME_SPRITES_MAX];
-
- bool_t highlighted;
- bool_t corrupt;
-} uisaveslot_t;
-
-/**
- * Initializes a save slot widget with the given slot index and display
- * file name.
- *
- * @param saveSlot The save slot widget to initialize.
- * @param slotIndex The save slot index this widget represents.
- * @param fileName Display file name; copied internally, safe to be
- * transient.
- */
-void uiSaveSlotInit(
- uisaveslot_t *saveSlot,
- const uint8_t slotIndex,
- const char_t *fileName
-);
-
-/**
- * Returns whether the save slot widget is highlighted.
- *
- * @param saveSlot The save slot widget to query.
- * @returns True if highlighted.
- */
-bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot);
-
-/**
- * Sets the highlighted state of the save slot widget.
- *
- * @param saveSlot The save slot widget to update.
- * @param highlighted The new highlighted state.
- */
-void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted);
-
-/**
- * Returns whether the save slot widget is flagged as corrupt.
- *
- * @param saveSlot The save slot widget to query.
- * @returns True if flagged as corrupt.
- */
-bool_t uiSaveSlotIsCorrupt(const uisaveslot_t *saveSlot);
-
-/**
- * Sets whether the save slot widget is flagged as corrupt - drawn in red
- * regardless of highlighted/display-type state, taking priority over both.
- *
- * @param saveSlot The save slot widget to update.
- * @param corrupt The new corrupt state.
- */
-void uiSaveSlotSetCorrupt(uisaveslot_t *saveSlot, const bool_t corrupt);
-
-/**
- * Returns the save slot widget's display type.
- *
- * @param saveSlot The save slot widget to query.
- * @returns The display type.
- */
-uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot);
-
-/**
- * Sets the save slot widget's display type.
- *
- * @param saveSlot The save slot widget to update.
- * @param displayType The new display type.
- */
-void uiSaveSlotSetDisplayType(
- uisaveslot_t *saveSlot,
- const uisaveslotdisplaytype_t displayType
-);
-
-/**
- * Draws the save slot widget as a framed box at the given position and
- * size, showing its slot number and file name.
- *
- * @param saveSlot The save slot widget to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @param width Frame width.
- * @param height Frame height.
- * @return Any error that occurs.
- */
-errorret_t uiSaveSlotDraw(
- uisaveslot_t *saveSlot,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-);
diff --git a/src/dusk/ui/dialog/save/uiselectsave.c b/src/dusk/ui/dialog/save/uiselectsave.c
deleted file mode 100644
index dcdf4c2c..00000000
--- a/src/dusk/ui/dialog/save/uiselectsave.c
+++ /dev/null
@@ -1,416 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiselectsave.h"
-#include "ui/widget/uiframe.h"
-#include "ui/widget/uimodal.h"
-#include "ui/dialog/uiconfirm.h"
-#include "ui/dialog/keyboard/uikeyboard.h"
-#include "ui/overlay/uifatalerror.h"
-#include "save/save.h"
-#include "time/timeepoch.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-uiselectsave_t UI_SELECT_SAVE;
-
-void uiSelectSaveFormatSlot(
- const uint8_t index,
- char_t *buffer,
- const size_t bufferSize
-) {
- saveslotcache_t *cache = &SAVE.caches[index];
-
- if(cache->corrupt) {
- errorret_t result = localeManagerGetString(
- "ui.select_save.corrupt", buffer, bufferSize, 0
- );
- if(errorIsNotOk(result)) errorCatch(result);
- return;
- }
-
- if(!saveSlotInUse(cache)) {
- errorret_t result = localeManagerGetString(
- "ui.select_save.empty", buffer, bufferSize, 0
- );
- if(errorIsNotOk(result)) errorCatch(result);
- return;
- }
-
- char_t dateText[32];
- timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText));
-
- errorret_t result = localeManagerGetText(
- "ui.select_save.slot_format", buffer, bufferSize, 0,
- cache->name, cache->playerLevel, dateText
- );
- if(errorIsNotOk(result)) errorCatch(result);
-}
-
-void uiSelectSaveRefreshSlot(const uint8_t index) {
- char_t fileName[UI_SAVE_SLOT_NAME_TEXT_MAX];
- uiSelectSaveFormatSlot(index, fileName, sizeof(fileName));
- uiSaveSlotInit(&UI_SELECT_SAVE.slots[index], index, fileName);
-
- uisaveslotdisplaytype_t displayType =
- UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE ?
- UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
- uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[index], displayType);
- uiSaveSlotSetCorrupt(
- &UI_SELECT_SAVE.slots[index], SAVE.caches[index].corrupt
- );
-}
-
-void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) {
- const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ?
- "ui.select_save.delete_mode" : "ui.confirm.cancel";
-
- errorret_t result = localeManagerGetString(
- labelKey, UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX, 0
- );
- if(errorIsNotOk(result)) errorCatch(result);
-
- UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button.label.dirty =
- true;
-}
-
-void uiSelectSaveSwitchType(const uiselectsavetype_t type) {
- UI_SELECT_SAVE.type = type;
-
- uisaveslotdisplaytype_t displayType = type == UI_SELECT_SAVE_TYPE_DELETE ?
- UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
- for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
- uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[i], displayType);
- }
-
- uiSelectSaveSetActionLabel(type);
-}
-
-void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
- if(!result) return;
-
- assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
- const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex;
-
- // uiSelectSaveOpen is only ever reached before any gameplay starts (see
- // uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
- // index is being written here - nothing else is relying on it.
- SAVE.slotCurrent = index;
- errorret_t initResult = saveSlotInit(&SAVE.slot);
- if(errorIsNotOk(initResult)) {
- errorCatch(errorPrint(initResult));
- uiFatalErrorOpen(initResult.state->message);
- return;
- }
-
- errorret_t writeResult = saveDeviceSlotWrite(
- &SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
- );
- if(errorIsNotOk(writeResult)) {
- errorCatch(errorPrint(writeResult));
- return;
- }
-
- SAVE.caches[index] = SAVE.slot.cachedData;
-
- uiSelectSaveSwitchType(UI_SELECT_SAVE_TYPE_LOAD);
- uiSelectSaveRefreshSlot(index);
- uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[index], true);
-}
-
-void uiSelectSaveConfirmDelete(const uint8_t index) {
- UI_SELECT_SAVE.pendingDeleteIndex = index;
- uiConfirmOpen(
- "ui.select_save.delete_confirm", uiSelectSaveDeleteConfirmed, NULL
- );
-}
-
-void uiSelectSaveNameEntered(
- const char_t *text,
- const bool_t confirmed,
- void *user
-) {
- if(!confirmed) return;
-
- const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
-
- // uiSelectSaveOpen is only ever reached before any gameplay starts (see
- // uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
- // index is being written here - nothing else is relying on it.
- SAVE.slotCurrent = index;
- errorret_t newGameResult = saveSlotNewGame();
- if(errorIsNotOk(newGameResult)) {
- errorCatch(errorPrint(newGameResult));
- uiFatalErrorOpen(newGameResult.state->message);
- return;
- }
- stringCopy(SAVE.slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
-
- // No save device available - proceed with an in-memory-only slot rather
- // than writing to a device that doesn't exist (see saveLoadSlot()'s own
- // comment on the same "continue without a save device" flow).
- if(SAVE.deviceCurrent != 0xFF) {
- errorret_t writeResult = saveDeviceSlotWrite(
- &SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
- );
- if(errorIsNotOk(writeResult)) {
- errorCatch(errorPrint(writeResult));
- uiFatalErrorOpen(writeResult.state->message);
- return;
- }
- }
-
- SAVE.caches[index] = SAVE.slot.cachedData;
- uiSelectSaveRefreshSlot(index);
-
- UI_SELECT_SAVE.result = index;
- uiSelectSaveClose();
-}
-
-void uiSelectSaveNameEmptySlot(const uint8_t index) {
- UI_SELECT_SAVE.pendingNameIndex = index;
-
- char_t title[UI_KEYBOARD_TITLE_TEXT_MAX];
- errorret_t result = localeManagerGetString(
- "ui.select_save.name_title", title, sizeof(title), 0
- );
- if(errorIsNotOk(result)) errorCatch(result);
-
- uikeyboardopen_t open = {
- .title = title,
- .onInput = uiSelectSaveNameEntered,
- .cancel = true,
- .maxLength = SAVE_SLOT_NAME_LENGTH,
- .trimmed = true,
- .allowBlank = false
- };
- uiKeyboardOpen(&open);
-}
-
-void uiSelectSaveWarnCorrupt(void) {
- const char_t *options[] = { "ui.confirm.confirm" };
- // Acknowledging just closes the warning - the player stays on the
- // select-save list rather than being pushed into naming/overwriting
- // the slot.
- uiModalOpen(
- "ui.select_save.corrupt", "ui.select_save.corrupt_warning",
- options, 1,
- NULL, NULL, NULL
- );
-}
-
-bool_t uiSelectSaveMenuCancelled(const uimenu_t *menu) {
- if(UI_SELECT_SAVE.type != UI_SELECT_SAVE_TYPE_DELETE) return false;
-
- uiSelectSaveSwitchType(UI_SELECT_SAVE_TYPE_LOAD);
- return true;
-}
-
-void uiSelectSaveSlotSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- if(UI_SELECT_SAVE.hasAction && index == UI_SELECT_SAVE_ACTION_INDEX) {
- uiSelectSaveSwitchType(
- UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_LOAD ?
- UI_SELECT_SAVE_TYPE_DELETE : UI_SELECT_SAVE_TYPE_LOAD
- );
- return;
- }
-
- if(UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE) {
- uiSelectSaveConfirmDelete(index);
- return;
- }
-
- if(SAVE.caches[index].corrupt) {
- uiSelectSaveWarnCorrupt();
- return;
- }
-
- if(!saveSlotInUse(&SAVE.caches[index])) {
- uiSelectSaveNameEmptySlot(index);
- return;
- }
-
- UI_SELECT_SAVE.result = index;
- uiSelectSaveClose();
-}
-
-void uiSelectSaveSlotChanged(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
- uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], i == index);
- }
-
- // The action row sits fixed below the scrollable list, so it never
- // needs to be scrolled into view.
- if(index >= SAVE_SLOT_COUNT) return;
-
- uiScrollingEnsureVisible(
- &UI_SELECT_SAVE.scroll, index, SAVE_SLOT_COUNT, UI_SELECT_SAVE_VISIBLE_ROWS
- );
-}
-
-void uiSelectSaveSlotClosed(const uimenu_t *menu) {
- for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
- uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], false);
- }
-
- if(UI_SELECT_SAVE.callback != NULL) {
- UI_SELECT_SAVE.callback(UI_SELECT_SAVE.result, UI_SELECT_SAVE.user);
- }
-}
-
-errorret_t uiSelectSaveInit(void) {
- memoryZero(&UI_SELECT_SAVE, sizeof(uiselectsave_t));
-
- uiLabelInit(
- &UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText,
- UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX
- );
- errorChain(localeManagerGetString(
- "ui.select_save.title",
- UI_SELECT_SAVE.titleText,
- UI_SELECT_SAVE_TITLE_TEXT_MAX,
- 0
- ));
- UI_SELECT_SAVE.titleLabel.dirty = true;
- uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel);
-
- uiMenuInit(
- &UI_SELECT_SAVE.slotMenu, uiSelectSaveSlotSelected, uiSelectSaveSlotClosed,
- uiSelectSaveSlotChanged
- );
- uiMenuSetCancelCallback(&UI_SELECT_SAVE.slotMenu, uiSelectSaveMenuCancelled);
- uiMenuSetItems(
- &UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, SAVE_SLOT_COUNT, 1
- );
- UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].type =
- UI_MENU_WIDGET_TYPE_BUTTON;
- uiButtonInit(
- &UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
- UI_SELECT_SAVE.actionText
- );
-
- uiScrollingInit(&UI_SELECT_SAVE.scroll);
-
- errorOk();
-}
-
-errorret_t uiSelectSaveDraw(void) {
- if(!uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) errorOk();
-
- const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- const float_t rowFrameHeight =
- (UI_FRAME_START_Y * 2) + (rowHeight * 2) + UI_FRAME_PADDING_Y;
- const float_t listHeight =
- (rowFrameHeight * (float_t)UI_SELECT_SAVE_VISIBLE_ROWS) +
- (UI_SELECT_SAVE_ROW_GAP * (float_t)(UI_SELECT_SAVE_VISIBLE_ROWS - 1));
-
- const float_t width = UI_SELECT_SAVE_WIDTH;
- const float_t height = (UI_FRAME_START_Y * 2)
- + (float_t)UI_SELECT_SAVE.titleLabel.height + UI_FRAME_PADDING_Y
- + listHeight
- + (UI_SELECT_SAVE.hasAction ? UI_FRAME_PADDING_Y + rowHeight : 0.0f);
-
- const float_t x = (float_t)SCREEN.scanX +
- ((float_t)SCREEN.scanWidth - width) * 0.5f;
- const float_t y = (float_t)SCREEN.scanY +
- ((float_t)SCREEN.scanHeight - height) * 0.5f;
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- const float_t contentX = x + UI_FRAME_START_X;
- const float_t contentY = y + UI_FRAME_START_Y;
- const float_t contentWidth = width - (UI_FRAME_START_X * 2);
-
- uiLabelSetX(&UI_SELECT_SAVE.titleLabel, contentX);
- uiLabelSetY(&UI_SELECT_SAVE.titleLabel, contentY);
- errorChain(uiLabelRender(&UI_SELECT_SAVE.titleLabel, COLOR_WHITE));
-
- const float_t listY = contentY + (float_t)UI_SELECT_SAVE.titleLabel.height +
- UI_FRAME_PADDING_Y;
-
- const uint8_t offset = UI_SELECT_SAVE.scroll.offset;
- uint8_t visibleEnd = (uint8_t)(offset + UI_SELECT_SAVE_VISIBLE_ROWS);
- if(visibleEnd > SAVE_SLOT_COUNT) visibleEnd = SAVE_SLOT_COUNT;
-
- for(uint8_t i = offset; i < visibleEnd; i++) {
- const float_t iy =
- listY + (float_t)(i - offset) * (rowFrameHeight + UI_SELECT_SAVE_ROW_GAP);
- errorChain(uiSaveSlotDraw(
- &UI_SELECT_SAVE.slots[i], contentX, iy, contentWidth, rowFrameHeight
- ));
- }
-
- if(UI_SELECT_SAVE.hasAction) {
- const float_t actionY = listY + listHeight + UI_FRAME_PADDING_Y;
- errorChain(uiButtonDraw(
- &UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
- contentX, actionY
- ));
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-bool_t uiSelectSaveIsOpen(void) {
- return uiMenuIsActive(&UI_SELECT_SAVE.slotMenu);
-}
-
-void uiSelectSaveOpen(
- const uiselectsavetype_t type,
- uiselectsaveresultcallback_t callback,
- void *user
-) {
- assertNotNull(callback, "Callback cannot be NULL");
- if(uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) return;
-
- UI_SELECT_SAVE.type = type;
- UI_SELECT_SAVE.callback = callback;
- UI_SELECT_SAVE.user = user;
- UI_SELECT_SAVE.result = UI_SELECT_SAVE_RESULT_NONE;
-
- for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
- uiSelectSaveRefreshSlot(i);
- }
-
- UI_SELECT_SAVE.hasAction =
- type == UI_SELECT_SAVE_TYPE_LOAD || type == UI_SELECT_SAVE_TYPE_DELETE;
- uint8_t itemCount = SAVE_SLOT_COUNT;
- if(UI_SELECT_SAVE.hasAction) {
- uiSelectSaveSetActionLabel(type);
- itemCount = UI_SELECT_SAVE_ITEM_CAPACITY;
- }
- uiMenuSetItems(
- &UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, itemCount, 1
- );
-
- uiScrollingInit(&UI_SELECT_SAVE.scroll);
- uiMenuOpen(&UI_SELECT_SAVE.slotMenu);
-}
-
-void uiSelectSaveClose(void) {
- uiMenuClose(&UI_SELECT_SAVE.slotMenu);
-}
-
-errorret_t uiSelectSaveDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/dialog/save/uiselectsave.h b/src/dusk/ui/dialog/save/uiselectsave.h
deleted file mode 100644
index ccc5bf31..00000000
--- a/src/dusk/ui/dialog/save/uiselectsave.h
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-#include "ui/widget/uimenu.h"
-#include "ui/widget/uiscrolling.h"
-#include "ui/dialog/save/uisaveslot.h"
-#include "save/slot/saveslot.h"
-
-#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64
-#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
-#define UI_SELECT_SAVE_ACTION_LABEL_MAX 32
-#define UI_SELECT_SAVE_VISIBLE_ROWS (SAVE_SLOT_COUNT < 3 ? SAVE_SLOT_COUNT : 3)
-#define UI_SELECT_SAVE_WIDTH 260.0f
-#define UI_SELECT_SAVE_ROW_GAP 4.0f
-#define UI_SELECT_SAVE_RESULT_NONE 0xFF
-
-// Slot rows plus one trailing action row (switch-to-delete/cancel), only
-// present for the LOAD and DELETE types - see uiSelectSaveOpen.
-#define UI_SELECT_SAVE_ITEM_CAPACITY (SAVE_SLOT_COUNT + 1)
-#define UI_SELECT_SAVE_ACTION_INDEX SAVE_SLOT_COUNT
-
-/**
- * The action the select-save panel is being used for. The caller
- * chooses this when calling uiSelectSaveOpen.
- */
-typedef enum {
- UI_SELECT_SAVE_TYPE_SAVE,
- UI_SELECT_SAVE_TYPE_LOAD,
- UI_SELECT_SAVE_TYPE_DELETE
-} uiselectsavetype_t;
-
-/**
- * Callback invoked once the select-save panel closes: slotIndex is the
- * chosen slot, or UI_SELECT_SAVE_RESULT_NONE if backed out of without
- * choosing.
- *
- * @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE.
- * @param user Arbitrary pointer passed to uiSelectSaveOpen.
- */
-typedef void (*uiselectsaveresultcallback_t)(
- const uint8_t slotIndex, void *user
-);
-
-typedef struct {
- uilabel_t titleLabel;
- char_t titleText[UI_SELECT_SAVE_TITLE_TEXT_MAX];
- spritebatchsprite_t titleSprites[UI_SELECT_SAVE_TITLE_SPRITES_MAX];
-
- uiselectsavetype_t type;
- bool_t hasAction;
- char_t actionText[UI_SELECT_SAVE_ACTION_LABEL_MAX];
-
- uimenu_t slotMenu;
- uimenuitem_t slotItems[UI_SELECT_SAVE_ITEM_CAPACITY];
- uisaveslot_t slots[SAVE_SLOT_COUNT];
- uiscrolling_t scroll;
- uint8_t pendingDeleteIndex;
- uint8_t pendingNameIndex;
-
- uiselectsaveresultcallback_t callback;
- void *user;
- uint8_t result;
-} uiselectsave_t;
-
-extern uiselectsave_t UI_SELECT_SAVE;
-
-/**
- * Initializes the select-save panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiSelectSaveInit(void);
-
-/**
- * Draws the select-save panel: a centered frame with a title, a
- * scrolling list of save/save.h's slots (each shown as a framed
- * uisaveslot_t, showing at most UI_SELECT_SAVE_VISIBLE_ROWS at a time),
- * and - when UI_SELECT_SAVE.hasAction is set - the switch-to-delete/
- * cancel action row fixed below the scrollable list, outside it. No-op
- * when not open.
- *
- * @return Any error that occurs.
- */
-errorret_t uiSelectSaveDraw(void);
-
-/**
- * Returns true when the select-save panel is currently open.
- *
- * @returns True if open.
- */
-bool_t uiSelectSaveIsOpen(void);
-
-/**
- * Opens the select-save panel for the given action, refreshing each row
- * from the current SAVE.caches state. callback is invoked exactly once
- * with the result, whether a slot was chosen or the panel was backed
- * out of without choosing.
- *
- * UI_SELECT_SAVE_TYPE_LOAD adds a trailing row to switch to
- * UI_SELECT_SAVE_TYPE_DELETE in place, and UI_SELECT_SAVE_TYPE_DELETE
- * adds a trailing "Cancel" row to switch back to
- * UI_SELECT_SAVE_TYPE_LOAD - neither closes the panel or invokes
- * callback, they just toggle UI_SELECT_SAVE.type and the slot rows'
- * styling. UI_SELECT_SAVE_TYPE_SAVE has no trailing row.
- *
- * @param type The action the caller is opening the panel for.
- * @param callback Called with the result once the panel closes. Cannot
- * be NULL.
- * @param user Arbitrary pointer passed through to callback.
- */
-void uiSelectSaveOpen(
- const uiselectsavetype_t type,
- uiselectsaveresultcallback_t callback,
- void *user
-);
-
-/**
- * Closes the select-save panel. No-op when already closed.
- */
-void uiSelectSaveClose(void);
-
-/**
- * Disposes of the select-save panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiSelectSaveDispose(void);
diff --git a/src/dusk/ui/dialog/uiconfirm.c b/src/dusk/ui/dialog/uiconfirm.c
deleted file mode 100644
index e7da213d..00000000
--- a/src/dusk/ui/dialog/uiconfirm.c
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiconfirm.h"
-#include "ui/widget/uimodal.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-
-#define UI_CONFIRM_INDEX_CONFIRM 0
-#define UI_CONFIRM_INDEX_CANCEL 1
-
-uiconfirm_t UI_CONFIRM;
-
-void uiConfirmModalCallback(const uint8_t optionIndex, void *user) {
- UI_CONFIRM.result = optionIndex == UI_CONFIRM_INDEX_CONFIRM;
- if(UI_CONFIRM.callback != NULL) {
- UI_CONFIRM.callback(UI_CONFIRM.result, user);
- }
-}
-
-errorret_t uiConfirmInit(void) {
- memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
- errorOk();
-}
-
-bool_t uiConfirmIsOpen(void) {
- return uiModalIsOpen();
-}
-
-bool_t uiConfirmGetResult(void) {
- return UI_CONFIRM.result;
-}
-
-void uiConfirmOpen(
- const char_t *question,
- uiconfirmcallback_t callback,
- void *user
-) {
- assertNotNull(question, "Question cannot be NULL");
-
- UI_CONFIRM.callback = callback;
- UI_CONFIRM.result = false;
-
- const char_t *options[] = {
- "ui.confirm.confirm",
- "ui.confirm.cancel"
- };
- uiModalOpen(
- NULL, question, options, sizeof(options) / sizeof(options[0]),
- uiConfirmModalCallback, NULL, user
- );
-}
-
-void uiConfirmClose(void) {
- uiModalClose(NULL);
-}
-
-errorret_t uiConfirmDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/dialog/uiconfirm.h b/src/dusk/ui/dialog/uiconfirm.h
deleted file mode 100644
index 3b76ac59..00000000
--- a/src/dusk/ui/dialog/uiconfirm.h
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-/**
- * Callback invoked once a confirm dialog is dismissed. Back/cancel
- * input is disabled while it's open, so the dialog can only be
- * dismissed by picking Confirm or Cancel.
- *
- * @param result True if Confirm was selected, false if Cancel was
- * selected.
- * @param user Arbitrary pointer passed to uiConfirmOpen.
- */
-typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
-
-typedef struct {
- uiconfirmcallback_t callback;
- bool_t result;
-} uiconfirm_t;
-
-extern uiconfirm_t UI_CONFIRM;
-
-/**
- * Initializes the confirm dialog.
- *
- * @return Any error that occurs.
- */
-errorret_t uiConfirmInit(void);
-
-/**
- * Returns true when the confirm dialog is currently open.
- *
- * @returns True if open.
- */
-bool_t uiConfirmIsOpen(void);
-
-/**
- * Returns the result of the most recently dismissed confirm dialog.
- *
- * @returns True if Confirm was selected, false otherwise.
- */
-bool_t uiConfirmGetResult(void);
-
-/**
- * Opens a confirm dialog, backed by the shared uimodal_t, with the given
- * question text and Confirm/Cancel buttons. callback is invoked exactly
- * once with the result once a button is selected.
- *
- * @param question Display text, or a locale message ID; copied
- * internally by uimodal, safe to be transient.
- * @param callback Called with the result once the dialog closes. May be
- * NULL.
- * @param user Arbitrary pointer passed through to callback.
- */
-void uiConfirmOpen(
- const char_t *question,
- uiconfirmcallback_t callback,
- void *user
-);
-
-/**
- * Closes the confirm dialog. No-op when already closed.
- */
-void uiConfirmClose(void);
-
-/**
- * Disposes of the confirm dialog.
- *
- * @return Any error that occurs.
- */
-errorret_t uiConfirmDispose(void);
diff --git a/src/dusk/ui/focus/CMakeLists.txt b/src/dusk/ui/focus/CMakeLists.txt
deleted file mode 100644
index 819217b7..00000000
--- a/src/dusk/ui/focus/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uifocus.c
-)
diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c
deleted file mode 100644
index 443ed035..00000000
--- a/src/dusk/ui/focus/uifocus.c
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uifocus.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "input/input.h"
-#include "time/time.h"
-
-const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = {
- { INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
- { INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
- { INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
- { INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
- { INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
-};
-
-uifocus_t UI_FOCUS;
-
-void uiFocusInit(void) {
- memoryZero(&UI_FOCUS, sizeof(uifocus_t));
-}
-
-uifocusitem_t * uiFocusPush(
- const uint8_t cols,
- const uint8_t rows,
- uifocusitemcallback_t selected,
- uifocusitemcallback_t changed,
- uifocusitemcallback_t closed,
- uifocusitemdirectioncallback_t direction,
- void *user
-) {
- assertTrue(
- UI_FOCUS.count < UI_FOCUS_STACK_MAX,
- "UI focus stack overflow"
- );
- assertTrue(cols > 0, "Focus item cols must be > 0");
- assertTrue(rows > 0, "Focus item rows must be > 0");
-
- uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count];
- memoryZero(item, sizeof(uifocusitem_t));
- item->cols = cols;
- item->rows = rows;
- item->selected = selected;
- item->changed = changed;
- item->closed = closed;
- item->direction = direction;
- item->user = user;
- UI_FOCUS.count++;
- UI_FOCUS.pushedThisTick = true;
- if(item->changed != NULL) item->changed(item);
- return item;
-}
-
-void uiFocusPop(void) {
- assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
-
- // Count is decremented before the closed callback fires so that a
- // push triggered from within it (e.g. reopening a menu once another
- // one reports it closed with no result) lands in the now-free slot
- // instead of one past it, which would silently strand the new item
- // above the tracked stack top.
- UI_FOCUS.count--;
- uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count];
- if(item->closed != NULL) item->closed(item);
-}
-
-void uiFocusPopItem(uifocusitem_t *item) {
- assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
- assertTrue(item >= UI_FOCUS.items, "Item is not on the focus stack");
- assertTrue(
- item < UI_FOCUS.items + UI_FOCUS.count,
- "Item is not on the focus stack"
- );
-
- while(&UI_FOCUS.items[UI_FOCUS.count - 1] != item) {
- uiFocusPop();
- }
- uiFocusPop();
-}
-
-void uiFocusSetPosition(uifocusitem_t *item, const uint8_t x, const uint8_t y) {
- assertTrue(UI_FOCUS.count > 0, "No active focus item");
- assertTrue(item->cols > 0, "Focus item cols must be > 0");
- assertTrue(item->rows > 0, "Focus item rows must be > 0");
-
- uint8_t newX = x % item->cols;
- uint8_t newY = y % item->rows;
-
- if(item->changed != NULL) {
- uint8_t oldX = item->x;
- uint8_t oldY = item->y;
- item->x = newX;
- item->y = newY;
- if(!item->changed(item)) {
- item->x = oldX;
- item->y = oldY;
- }
- return;
- }
-
- item->x = newX;
- item->y = newY;
-}
-
-void uiFocusMoveDirection(
- uifocusitem_t *item,
- const uifocusdirection_t dir
-) {
- for(
- const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
- m->action != INPUT_ACTION_NULL;
- m++
- ) {
- if(m->direction != dir) continue;
-
- // Widened to int16_t so a move off the left/top edge (e.g. x=0,
- // dx=-1) stays negative here instead of wrapping straight to 255 via
- // a uint8_t truncation - that would then land on `255 % cols`
- // (column 3 of a 14-wide grid, not the intended last column) rather
- // than wrapping to the row/column's actual last cell.
- int16_t x = (int16_t)item->x + m->dx;
- int16_t y = (int16_t)item->y + m->dy;
- if(x < 0) x += item->cols;
- if(y < 0) y += item->rows;
-
- uiFocusSetPosition(item, (uint8_t)x, (uint8_t)y);
- break;
- }
-}
-
-void uiFocusHandleDirection(
- uifocusitem_t *item,
- const uifocusdirection_t dir
-) {
- if(item->direction != NULL && item->direction(item, dir)) return;
- uiFocusMoveDirection(item, dir);
-}
-
-void uiFocusUpdate(void) {
- if(UI_FOCUS.count == 0) return;
-
- #ifdef DUSK_TIME_DYNAMIC
- if(TIME.dynamicUpdate) return;
- #endif
-
- if(UI_FOCUS.pushedThisTick) {
- UI_FOCUS.pushedThisTick = false;
- return;
- }
-
- uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
-
- if(inputPressed(INPUT_ACTION_ACCEPT)) {
- if(item->selected != NULL) item->selected(item);
- return;
- }
-
- if(inputPressed(INPUT_ACTION_CANCEL)) {
- if(item->cancel != NULL && item->cancel(item)) return;
- if(!item->disableBack) uiFocusPop();
- return;
- }
-
- for(
- const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
- m->action != INPUT_ACTION_NULL;
- m++
- ) {
- if(!inputPressed(m->action)) continue;
- UI_FOCUS.direction = m->direction;
- UI_FOCUS.timeHeld = 0.0f;
- uiFocusHandleDirection(item, m->direction);
- return;
- }
-
- bool_t held = false;
- for(
- const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
- m->action != INPUT_ACTION_NULL;
- m++
- ) {
- if(m->direction != UI_FOCUS.direction) continue;
- held = inputIsDown(m->action);
- break;
- }
-
- if(!held) {
- UI_FOCUS.direction = UI_FOCUS_DIRECTION_NONE;
- UI_FOCUS.timeHeld = 0.0f;
- return;
- }
-
- UI_FOCUS.timeHeld += TIME.delta;
-
- if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return;
- if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return;
- UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY;
- uiFocusHandleDirection(item, UI_FOCUS.direction);
-}
diff --git a/src/dusk/ui/focus/uifocus.h b/src/dusk/ui/focus/uifocus.h
deleted file mode 100644
index d68de640..00000000
--- a/src/dusk/ui/focus/uifocus.h
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "uifocusitem.h"
-#include "input/inputaction.h"
-
-/** Maximum depth of the focus stack. */
-#define UI_FOCUS_STACK_MAX 8
-
-/**
- * How long a direction must be held before repeating begins, in seconds.
- */
-#define UI_FOCUS_HOLD_DELAY 0.5f
-
-/**
- * Interval between repeated moves while a direction is held, in seconds.
- */
-#define UI_FOCUS_HOLD_REPEAT 0.1f
-
-typedef struct {
- inputaction_t action;
- uifocusdirection_t direction;
- int8_t dx;
- int8_t dy;
-} uifocusdirmap_t;
-
-/**
- * Mapping of input actions to focus directions, terminated by an
- * entry with action == INPUT_ACTION_NULL.
- */
-extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[];
-
-/**
- * A stack of focused UI items. Push an item when a widget captures
- * focus; pop it when focus is released. The topmost item is always
- * the active focus context.
- */
-typedef struct {
- uifocusitem_t items[UI_FOCUS_STACK_MAX];
- uint8_t count;
- uifocusdirection_t direction;
- float_t timeHeld;
- bool_t pushedThisTick;
-} uifocus_t;
-
-extern uifocus_t UI_FOCUS;
-
-/**
- * Initializes the focus system, zeroing all state.
- */
-void uiFocusInit(void);
-
-/**
- * Pushes a new focus item onto the stack with the given grid dimensions
- * and callbacks. x and y are initialized to 0.
- *
- * @param cols Number of columns in the focus grid.
- * @param rows Number of rows in the focus grid.
- * @param selected Called when the user selects the focused cell.
- * @param changed Called when the focused cell position changes.
- * @param closed Called when this focus item is popped.
- * @param direction Called on a direction press/hold before the default
- * cell-to-cell movement is applied; may be NULL.
- * @param user Arbitrary pointer stored on the item before changed fires.
- * @returns Pointer to the newly pushed focus item.
- */
-uifocusitem_t * uiFocusPush(
- const uint8_t cols,
- const uint8_t rows,
- uifocusitemcallback_t selected,
- uifocusitemcallback_t changed,
- uifocusitemcallback_t closed,
- uifocusitemdirectioncallback_t direction,
- void *user
-);
-
-/**
- * Pops the topmost focus item from the stack, invoking its closed
- * callback if one is set.
- */
-void uiFocusPop(void);
-
-/**
- * Pops an item and anything that was pushed after it from the focus stack.
- *
- * @param item The focus item to pop to. Must be on the stack.
- */
-void uiFocusPopItem(uifocusitem_t *item);
-
-/**
- * Manually sets the cursor position of the topmost focus item and
- * fires its changed callback.
- *
- * @param x Column to move to.
- * @param y Row to move to.
- */
-void uiFocusSetPosition(uifocusitem_t *item, const uint8_t x, const uint8_t y);
-
-/**
- * Moves the topmost focus item one step in the given direction,
- * wrapping at the grid edges, and fires its changed callback.
- *
- * @param item The focus item to move.
- * @param dir Direction to move.
- */
-void uiFocusMoveDirection(
- uifocusitem_t *item,
- const uifocusdirection_t dir
-);
-
-/**
- * Handles a direction press/hold for the given item: gives the item's
- * direction callback (if any) first refusal, falling back to the
- * default cell-to-cell movement if it's unset or returns false.
- *
- * @param item The focus item to move.
- * @param dir Direction that was pressed or held.
- */
-void uiFocusHandleDirection(
- uifocusitem_t *item,
- const uifocusdirection_t dir
-);
-
-/**
- * Updates the focus system. Handles first-press movement and
- * held-direction repeating. Called once per game tick.
- */
-void uiFocusUpdate(void);
diff --git a/src/dusk/ui/focus/uifocusitem.h b/src/dusk/ui/focus/uifocusitem.h
deleted file mode 100644
index 32654321..00000000
--- a/src/dusk/ui/focus/uifocusitem.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "dusk.h"
-
-typedef struct uifocusitem_s uifocusitem_t;
-
-typedef enum {
- UI_FOCUS_DIRECTION_NONE,
- UI_FOCUS_DIRECTION_UP,
- UI_FOCUS_DIRECTION_DOWN,
- UI_FOCUS_DIRECTION_LEFT,
- UI_FOCUS_DIRECTION_RIGHT
-} uifocusdirection_t;
-
-/**
- * Callback invoked when a focus item's selected cell changes.
- *
- * @param item The focus item that changed.
- */
-typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item);
-
-/**
- * Callback invoked when a direction is pressed or held-repeated while
- * this item is focused, before the default cell-to-cell movement is
- * applied.
- *
- * @param item The focus item that is currently focused.
- * @param direction The direction that was pressed.
- * @returns True if the direction was fully handled and the default
- * grid movement should be skipped; false to fall through to it.
- */
-typedef bool_t (*uifocusitemdirectioncallback_t)(
- const uifocusitem_t *item,
- const uifocusdirection_t direction
-);
-
-/**
- * A single entry on the UI focus stack. Tracks the focused cell
- * within a grid of cols x rows, and the current x/y position.
- */
-struct uifocusitem_s {
- uint8_t cols;
- uint8_t rows;
- uint8_t x;
- uint8_t y;
- uifocusitemcallback_t selected;
- uifocusitemcallback_t changed;
- uifocusitemcallback_t closed;
- uifocusitemdirectioncallback_t direction;
-
- // Called when INPUT_ACTION_CANCEL is pressed, before disableBack/the
- // default pop is applied; may be NULL. Returns true if fully handled
- // (e.g. it did something else instead, or popped itself), which skips
- // the default pop; false to fall through to the default disableBack/
- // pop behavior below.
- uifocusitemcallback_t cancel;
-
- void *user;
-
- // When true, INPUT_ACTION_CANCEL is ignored while this item is the
- // topmost focus item - it does not pop. Does not affect a
- // programmatic uiFocusPop()/uiFocusPopItem() call. Only consulted
- // when cancel is unset or returns false.
- bool_t disableBack;
-};
diff --git a/src/dusk/ui/overlay/CMakeLists.txt b/src/dusk/ui/overlay/CMakeLists.txt
deleted file mode 100644
index 2c49abf3..00000000
--- a/src/dusk/ui/overlay/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uicrop.c
- uifullbox.c
- uiloading.c
- uifatalerror.c
-)
diff --git a/src/dusk/ui/overlay/uicrop.c b/src/dusk/ui/overlay/uicrop.c
deleted file mode 100644
index 3e8d388b..00000000
--- a/src/dusk/ui/overlay/uicrop.c
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uicrop.h"
-#include "display/screen/screen.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-#include "display/texture/texture.h"
-
-uicrop_t UI_CROP;
-
-errorret_t uiCropInit(void) {
- UI_CROP.color = COLOR_BLACK;
- errorOk();
-}
-
-errorret_t uiCropDraw(void) {
- #ifndef DUSK_DISPLAY_SIZE_DYNAMIC
- errorOk();
- #endif
-
- if(
- SCREEN.scanX == 0 &&
- SCREEN.scanY == 0 &&
- SCREEN.scanWidth == SCREEN.width &&
- SCREEN.scanHeight == SCREEN.height
- ) errorOk();
-
- float_t x0 = (float_t)SCREEN.scanX;
- float_t y0 = (float_t)SCREEN.scanY;
- float_t x1 = (float_t)(SCREEN.scanX + SCREEN.scanWidth);
- float_t y1 = (float_t)(SCREEN.scanY + SCREEN.scanHeight);
- float_t w = (float_t)SCREEN.width;
- float_t h = (float_t)SCREEN.height;
-
- spritebatchsprite_t sprites[4];
- int32_t count = 0;
-
- if(SCREEN.scanX > 0) {
- sprites[count++] = (spritebatchsprite_t){
- .min = { 0.0f, 0.0f, 0.0f },
- .max = { x0, h, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- }
-
- if(SCREEN.scanX + SCREEN.scanWidth < SCREEN.width) {
- sprites[count++] = (spritebatchsprite_t){
- .min = { x1, 0.0f, 0.0f },
- .max = { w, h, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- }
-
- if(SCREEN.scanY > 0) {
- sprites[count++] = (spritebatchsprite_t){
- .min = { x0, 0.0f, 0.0f },
- .max = { x1, y0, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- }
-
- if(SCREEN.scanY + SCREEN.scanHeight < SCREEN.height) {
- sprites[count++] = (spritebatchsprite_t){
- .min = { x0, y1, 0.0f },
- .max = { x1, h, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- }
-
- if(count == 0) errorOk();
-
- shadermaterial_t material = {
- .unlit = {
- .color = UI_CROP.color,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/overlay/uicrop.h b/src/dusk/ui/overlay/uicrop.h
deleted file mode 100644
index ef822633..00000000
--- a/src/dusk/ui/overlay/uicrop.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/color.h"
-
-typedef struct {
- color_t color;
-} uicrop_t;
-
-extern uicrop_t UI_CROP;
-
-/**
- * Initializes the crop bars to opaque black.
- *
- * @return Any error that occurs.
- */
-errorret_t uiCropInit(void);
-
-/**
- * Renders solid-color bars covering every area outside the
- * current scan-safe region. No-ops when the scan area equals
- * the full viewport.
- *
- * @return Any error that occurs.
- */
-errorret_t uiCropDraw(void);
diff --git a/src/dusk/ui/overlay/uifatalerror.c b/src/dusk/ui/overlay/uifatalerror.c
deleted file mode 100644
index efc59171..00000000
--- a/src/dusk/ui/overlay/uifatalerror.c
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uifatalerror.h"
-#include "ui/widget/uiframe.h"
-#include "ui/widget/uibutton.h"
-#include "engine/engine.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "util/math.h"
-#include "display/screen/screen.h"
-#include "display/color.h"
-#include "display/text/font.h"
-#include "display/spritebatch/spritebatch.h"
-
-uifatalerror_t UI_FATAL_ERROR;
-
-void uiFatalErrorMenuSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- ENGINE.running = false;
-}
-
-errorret_t uiFatalErrorInit(void) {
- memoryZero(&UI_FATAL_ERROR, sizeof(uifatalerror_t));
-
- uiLabelInit(
- &UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageText,
- UI_FATAL_ERROR.messageSprites, UI_FATAL_ERROR_MESSAGE_SPRITES_MAX
- );
-
- errorOk();
-}
-
-void uiFatalErrorOpen(const char_t *message) {
- // This is the last line of defense - it must never itself fail, so a
- // second fatal error while one is already showing is silently dropped
- // rather than asserting.
- if(UI_FATAL_ERROR.open) return;
- if(message == NULL) message = UI_FATAL_ERROR_GENERIC_MESSAGE;
-
- stringCopy(
- UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1
- );
-
- const float_t wrapWidth =
- (float_t)SCREEN.scanWidth * UI_FATAL_ERROR_MESSAGE_WRAP_RATIO;
- uiLabelSetWrap(
- &UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageWrapText,
- UI_FATAL_ERROR_MESSAGE_MAX, wrapWidth
- );
- uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel);
-
- UI_FATAL_ERROR.open = true;
-
- uiMenuInit(&UI_FATAL_ERROR.menu, uiFatalErrorMenuSelected, NULL, NULL);
- UI_FATAL_ERROR.items[0].type = UI_MENU_WIDGET_TYPE_BUTTON;
- uiButtonInit(&UI_FATAL_ERROR.items[0].button, UI_FATAL_ERROR_QUIT_LABEL);
- uiMenuSetItems(
- &UI_FATAL_ERROR.menu, UI_FATAL_ERROR.items, UI_FATAL_ERROR_ITEM_COUNT, 1
- );
- // No way out but the QUIT button - this is a fatal, unrecoverable
- // condition.
- uiMenuSetDisableBack(&UI_FATAL_ERROR.menu, true);
- uiMenuOpen(&UI_FATAL_ERROR.menu);
-}
-
-bool_t uiFatalErrorIsOpen(void) {
- return UI_FATAL_ERROR.open;
-}
-
-errorret_t uiFatalErrorDraw(void) {
- if(!UI_FATAL_ERROR.open) errorOk();
-
- const float_t x = (float_t)SCREEN.scanX;
- const float_t y = (float_t)SCREEN.scanY;
- const float_t width = (float_t)SCREEN.scanWidth;
- const float_t height = (float_t)SCREEN.scanHeight;
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- const float_t contentWidth = mathMax(
- (float_t)UI_FATAL_ERROR.messageLabel.width,
- (float_t)UI_FATAL_ERROR.items[0].button.label.width
- );
- const float_t contentHeight = (float_t)UI_FATAL_ERROR.messageLabel.height +
- UI_FRAME_PADDING_Y + rowHeight;
-
- const float_t contentX = x + (width - contentWidth) * 0.5f;
- const float_t contentY = y + (height - contentHeight) * 0.5f;
-
- uiLabelSetX(&UI_FATAL_ERROR.messageLabel, contentX);
- uiLabelSetY(&UI_FATAL_ERROR.messageLabel, contentY);
- errorChain(uiLabelRender(&UI_FATAL_ERROR.messageLabel, COLOR_WHITE));
-
- const float_t menuY = contentY +
- (float_t)UI_FATAL_ERROR.messageLabel.height + UI_FRAME_PADDING_Y;
- errorChain(uiMenuDraw(
- &UI_FATAL_ERROR.menu, contentX, menuY, contentWidth, rowHeight
- ));
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-errorret_t uiFatalErrorDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/overlay/uifatalerror.h b/src/dusk/ui/overlay/uifatalerror.h
deleted file mode 100644
index 85e8b6b1..00000000
--- a/src/dusk/ui/overlay/uifatalerror.h
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-#include "ui/widget/uimenu.h"
-
-#define UI_FATAL_ERROR_MESSAGE_MAX 256
-#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX
-#define UI_FATAL_ERROR_MESSAGE_WRAP_RATIO 0.8f
-#define UI_FATAL_ERROR_ITEM_COUNT 1
-#define UI_FATAL_ERROR_QUIT_LABEL "QUIT"
-#define UI_FATAL_ERROR_GENERIC_MESSAGE \
- "An unexpected error occurred and the game must exit. " \
- "Please contact support if this issue persists."
-
-typedef struct {
- bool_t open;
-
- uilabel_t messageLabel;
- char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX];
- char_t messageWrapText[UI_FATAL_ERROR_MESSAGE_MAX];
- spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX];
-
- uimenu_t menu;
- uimenuitem_t items[UI_FATAL_ERROR_ITEM_COUNT];
-} uifatalerror_t;
-
-extern uifatalerror_t UI_FATAL_ERROR;
-
-/**
- * Initializes the fatal error overlay.
- *
- * @return Any error that occurs.
- */
-errorret_t uiFatalErrorInit(void);
-
-/**
- * Opens the fatal error overlay, a fullscreen, non-dismissible display
- * for an unrecoverable error - the only way out is its QUIT button,
- * which sets ENGINE.running to false. Intended as a last-resort, nicer-
- * looking alternative to a raw assert crash for a genuinely fatal
- * condition, so it must never itself fail: a second call while already
- * open is silently ignored rather than asserting, and it draws above
- * every other UI element.
- *
- * @param message The error text to display; copied internally, safe to
- * be transient. Truncated to UI_FATAL_ERROR_MESSAGE_MAX - 1 characters.
- * May be NULL, in which case UI_FATAL_ERROR_GENERIC_MESSAGE is shown
- * instead - useful for callers with no specific error text to report
- * (or that don't want to risk formatting/locale-lookup failures of their
- * own right before calling this, the last line of defense).
- */
-void uiFatalErrorOpen(const char_t *message);
-
-/**
- * Returns whether the fatal error overlay is currently open.
- *
- * @returns True if open.
- */
-bool_t uiFatalErrorIsOpen(void);
-
-/**
- * Draws the fatal error overlay. No-op when not open.
- *
- * @return Any error that occurs.
- */
-errorret_t uiFatalErrorDraw(void);
-
-/**
- * Disposes of the fatal error overlay.
- *
- * @return Any error that occurs.
- */
-errorret_t uiFatalErrorDispose(void);
diff --git a/src/dusk/ui/overlay/uifullbox.c b/src/dusk/ui/overlay/uifullbox.c
deleted file mode 100644
index d1751f0b..00000000
--- a/src/dusk/ui/overlay/uifullbox.c
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uifullbox.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "display/screen/screen.h"
-#include "display/texture/texture.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-
-uifullbox_t UI_FULLBOX_UNDER;
-uifullbox_t UI_FULLBOX_OVER;
-
-void uiFullboxInit(uifullbox_t *fullbox) {
- assertNotNull(fullbox, "fullbox must not be NULL");
- memoryZero(fullbox, sizeof(uifullbox_t));
-}
-
-void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
- assertNotNull(fullbox, "fullbox must not be NULL");
- if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) return;
-
- fullbox->time += delta;
- if(fullbox->time >= fullbox->duration) {
- fullbox->time = fullbox->duration;
- if(fullbox->onTransitionEnd) {
- fullbox->onTransitionEnd(fullbox, fullbox->onTransitionEndUser);
- }
- }
-}
-
-color_t uiFullboxGetColor(const uifullbox_t *fullbox) {
- if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) {
- return fullbox->toColor;
- }
- float_t t = easingApply(fullbox->easing, fullbox->time / fullbox->duration);
- return color4b(
- (uint8_t)((float_t)fullbox->fromColor.r +
- ((float_t)fullbox->toColor.r - (float_t)fullbox->fromColor.r) * t),
- (uint8_t)((float_t)fullbox->fromColor.g +
- ((float_t)fullbox->toColor.g - (float_t)fullbox->fromColor.g) * t),
- (uint8_t)((float_t)fullbox->fromColor.b +
- ((float_t)fullbox->toColor.b - (float_t)fullbox->fromColor.b) * t),
- (uint8_t)((float_t)fullbox->fromColor.a +
- ((float_t)fullbox->toColor.a - (float_t)fullbox->fromColor.a) * t)
- );
-}
-
-errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
- assertNotNull(fullbox, "fullbox must not be NULL");
-
- color_t color = uiFullboxGetColor(fullbox);
- if(color.a == 0) errorOk();
-
- spritebatchsprite_t sprite = {
- .min = { 0.0f, 0.0f, 0.0f },
- .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t material = {
- .unlit = {
- .color = color,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- return spriteBatchFlush();
-}
-
-void uiFullboxTransition(
- uifullbox_t *fullbox,
- color_t from,
- color_t to,
- float_t duration,
- easingtype_t easing
-) {
- assertNotNull(fullbox, "fullbox must not be NULL");
- fullbox->fromColor = from;
- fullbox->toColor = to;
- fullbox->duration = duration;
- fullbox->time = 0.0f;
- fullbox->easing = easing;
-}
-
-errorret_t uiFullboxUnderInit(void) {
- uiFullboxInit(&UI_FULLBOX_UNDER);
- errorOk();
-}
-
-errorret_t uiFullboxUnderUpdate(void) {
- uiFullboxUpdate(&UI_FULLBOX_UNDER, TIME.delta);
- errorOk();
-}
-
-errorret_t uiFullboxUnderDraw(void) {
- return uiFullboxDraw(&UI_FULLBOX_UNDER);
-}
-
-errorret_t uiFullboxOverInit(void) {
- uiFullboxInit(&UI_FULLBOX_OVER);
- errorOk();
-}
-
-errorret_t uiFullboxOverUpdate(void) {
- uiFullboxUpdate(&UI_FULLBOX_OVER, TIME.delta);
- errorOk();
-}
-
-errorret_t uiFullboxOverDraw(void) {
- return uiFullboxDraw(&UI_FULLBOX_OVER);
-}
diff --git a/src/dusk/ui/overlay/uifullbox.h b/src/dusk/ui/overlay/uifullbox.h
deleted file mode 100644
index f27a1466..00000000
--- a/src/dusk/ui/overlay/uifullbox.h
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/color.h"
-#include "animation/easing.h"
-
-typedef struct uifullbox_s uifullbox_t;
-
-/**
- * Callback fired once when a fullbox transition completes.
- *
- * @param fullbox The uifullbox_t the transition ran on.
- * @param user The user pointer passed alongside the callback.
- */
-typedef void (*uifullboxcallback_t)(uifullbox_t *fullbox, void *user);
-
-struct uifullbox_s {
- color_t fromColor;
- color_t toColor;
- float_t duration;
- float_t time;
- easingtype_t easing;
- uifullboxcallback_t onTransitionEnd;
- void *onTransitionEndUser;
-};
-
-extern uifullbox_t UI_FULLBOX_UNDER;
-extern uifullbox_t UI_FULLBOX_OVER;
-
-/**
- * Initializes a fullbox, zeroing all fields.
- *
- * @param fullbox The fullbox to initialize.
- */
-void uiFullboxInit(uifullbox_t *fullbox);
-
-/**
- * Advances the fullbox transition. Fires onTransitionEnd once when the
- * transition completes. Safe to call when no transition is running.
- *
- * @param fullbox The fullbox to update.
- * @param delta Seconds elapsed since last update.
- */
-void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta);
-
-/**
- * Computes the fullbox's current color, interpolating between fromColor and
- * toColor based on the transition's elapsed time and easing function.
- *
- * @param fullbox The fullbox to compute the color for.
- * @return The current interpolated color.
- */
-color_t uiFullboxGetColor(const uifullbox_t *fullbox);
-
-/**
- * Renders the fullbox. Skipped entirely when the current alpha is zero.
- *
- * @param fullbox The fullbox to draw.
- * @return Any error that occurs.
- */
-errorret_t uiFullboxDraw(uifullbox_t *fullbox);
-
-/**
- * Begins a color transition.
- *
- * @param fullbox The fullbox to transition.
- * @param from Start color.
- * @param to End color.
- * @param duration Transition duration in seconds.
- * @param easing Easing function to apply.
- */
-void uiFullboxTransition(
- uifullbox_t *fullbox,
- color_t from,
- color_t to,
- float_t duration,
- easingtype_t easing
-);
-
-/**
- * Init function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxUnderInit(void);
-
-/**
- * Update function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxUnderUpdate(void);
-
-/**
- * Draw function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxUnderDraw(void);
-
-/**
- * Init function for UI_FULLBOX_OVER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxOverInit(void);
-
-/**
- * Update function for UI_FULLBOX_OVER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxOverUpdate(void);
-
-/**
- * Draw function for UI_FULLBOX_OVER (registered in UI_ELEMENTS).
- *
- * @return Any error that occurs.
- */
-errorret_t uiFullboxOverDraw(void);
diff --git a/src/dusk/ui/overlay/uiloading.c b/src/dusk/ui/overlay/uiloading.c
deleted file mode 100644
index 70927eac..00000000
--- a/src/dusk/ui/overlay/uiloading.c
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiloading.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "display/text/text.h"
-#include "display/screen/screen.h"
-#include "display/spritebatch/spritebatch.h"
-#include "uifullbox.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-uiloading_t UI_LOADING;
-
-errorret_t uiLoadingInit(void) {
- memoryZero(&UI_LOADING, sizeof(uiloading_t));
-
- errorChain(localeManagerGetString(
- "ui.loading.text", UI_LOADING.text, UI_LOADING_TEXT_MAX, 0
- ));
-
- errorOk();
-}
-
-errorret_t uiLoadingUpdate(void) {
- if(UI_LOADING.duration > 0.0f && UI_LOADING.time < UI_LOADING.duration) {
- UI_LOADING.time += TIME.delta;
- if(UI_LOADING.time >= UI_LOADING.duration) {
- UI_LOADING.time = UI_LOADING.duration;
- if(UI_LOADING.onTransitionEnd) {
- UI_LOADING.onTransitionEnd(&UI_LOADING, UI_LOADING.onTransitionEndUser);
- }
- }
- }
- errorOk();
-}
-
-errorret_t uiLoadingDraw(void) {
- float_t alpha;
- if(UI_LOADING.duration <= 0.0f || UI_LOADING.time >= UI_LOADING.duration) {
- alpha = UI_LOADING.toAlpha;
- } else {
- float_t t = UI_LOADING.time / UI_LOADING.duration;
- alpha = UI_LOADING.fromAlpha +
- (UI_LOADING.toAlpha - UI_LOADING.fromAlpha) * t;
- }
-
- if(alpha <= 0.0f) errorOk();
-
- int32_t textW, textH;
- textMeasure(UI_LOADING.text, &FONT_DEFAULT, &textW, &textH);
-
- float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) -
- (float_t)textW - UI_LOADING_MARGIN;
- float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) -
- (float_t)textH - UI_LOADING_MARGIN;
-
- color_t color = COLOR_WHITE;
- color.a = (uint8_t)(alpha * 255.0f);
-
- errorChain(textDraw(x, y, UI_LOADING.text, color, &FONT_DEFAULT));
- return spriteBatchFlush();
-}
-
-void uiLoadingTransition(
- const float_t from,
- const float_t to,
- const uiloadingcallback_t callback,
- void *user
-) {
- UI_LOADING.fromAlpha = from;
- UI_LOADING.toAlpha = to;
- UI_LOADING.duration = UI_LOADING_FADE_DURATION;
- UI_LOADING.time = 0.0f;
- UI_LOADING.onTransitionEnd = callback;
- UI_LOADING.onTransitionEndUser = user;
-}
-
-void uiLoadingShow(uiloadingcallback_t callback, void *user) {
- uiLoadingTransition(0.0f, 1.0f, callback, user);
- uiFullboxTransition(
- &UI_FULLBOX_OVER,
- COLOR_TRANSPARENT_BLACK,
- COLOR_BLACK,
- UI_LOADING_FADE_DURATION,
- EASING_LINEAR
- );
-}
-
-void uiLoadingHide(uiloadingcallback_t callback, void *user) {
- uiLoadingTransition(1.0f, 0.0f, callback, user);
- uiFullboxTransition(
- &UI_FULLBOX_OVER,
- COLOR_BLACK,
- COLOR_TRANSPARENT_BLACK,
- UI_LOADING_FADE_DURATION,
- EASING_LINEAR
- );
-}
diff --git a/src/dusk/ui/overlay/uiloading.h b/src/dusk/ui/overlay/uiloading.h
deleted file mode 100644
index 531d4f23..00000000
--- a/src/dusk/ui/overlay/uiloading.h
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_LOADING_FADE_DURATION 0.5f
-#define UI_LOADING_MARGIN 8.0f
-#define UI_LOADING_TEXT_MAX 32
-
-typedef struct uiloading_s uiloading_t;
-
-/**
- * Callback fired once when a loading indicator fade transition completes.
- *
- * @param loading The uiloading_t the transition ran on.
- * @param user The user pointer passed alongside the callback.
- */
-typedef void (*uiloadingcallback_t)(uiloading_t *loading, void *user);
-
-struct uiloading_s {
- float_t fromAlpha;
- float_t toAlpha;
- float_t duration;
- float_t time;
- uiloadingcallback_t onTransitionEnd;
- void *onTransitionEndUser;
- char_t text[UI_LOADING_TEXT_MAX];
-};
-
-extern uiloading_t UI_LOADING;
-
-/**
- * Initializes the loading indicator.
- *
- * @return Any error that occurs.
- */
-errorret_t uiLoadingInit(void);
-
-/**
- * Advances the loading indicator fade transition. Fires onTransitionEnd once
- * when the transition completes.
- *
- * @return Any error that occurs.
- */
-errorret_t uiLoadingUpdate(void);
-
-/**
- * Draws the loading indicator. No-op when fully transparent.
- *
- * @return Error state.
- */
-errorret_t uiLoadingDraw(void);
-
-/**
- * Begins a loading indicator fade transition, replacing any transition
- * already in progress along with its pending callback.
- *
- * @param from Starting alpha.
- * @param to Ending alpha.
- * @param callback Called when the transition completes. May be NULL.
- * @param user Forwarded to the callback unchanged.
- */
-void uiLoadingTransition(
- const float_t from,
- const float_t to,
- const uiloadingcallback_t callback,
- void *user
-);
-
-/**
- * Fades the loading indicator in. Invokes callback when fully visible.
- *
- * @param callback Called when the fade-in completes. May be NULL.
- * @param user Forwarded to the callback unchanged.
- */
-void uiLoadingShow(uiloadingcallback_t callback, void *user);
-
-/**
- * Fades the loading indicator out. Invokes callback when fully hidden.
- *
- * @param callback Called when the fade-out completes. May be NULL.
- * @param user Forwarded to the callback unchanged.
- */
-void uiLoadingHide(uiloadingcallback_t callback, void *user);
diff --git a/src/dusk/ui/rpg/CMakeLists.txt b/src/dusk/ui/rpg/CMakeLists.txt
deleted file mode 100644
index eb3441de..00000000
--- a/src/dusk/ui/rpg/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uiemoji.c
-)
-
-
-add_subdirectory(textbox)
\ No newline at end of file
diff --git a/src/dusk/ui/rpg/textbox/CMakeLists.txt b/src/dusk/ui/rpg/textbox/CMakeLists.txt
deleted file mode 100644
index e7787f2d..00000000
--- a/src/dusk/ui/rpg/textbox/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uitextbox.c
- uitextboxmain.c
- uitextboxmini.c
- uitextboxminilist.c
-)
diff --git a/src/dusk/ui/rpg/textbox/uitextbox.c b/src/dusk/ui/rpg/textbox/uitextbox.c
deleted file mode 100644
index 20b078e5..00000000
--- a/src/dusk/ui/rpg/textbox/uitextbox.c
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitextbox.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "time/time.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-#include "ui/widget/uiframe.h"
-
-void uiTextboxInit(
- uitextbox_t *box,
- char_t *text,
- const uint32_t maxLength,
- uitextboxline_t *lines,
- const uint32_t linesMax
-) {
- assertNotNull(box, "Textbox cannot be NULL");
- assertNotNull(text, "Text buffer cannot be NULL");
- assertTrue(maxLength >= 1, "maxLength must be at least 1");
- assertNotNull(lines, "Lines buffer cannot be NULL");
- assertTrue(linesMax >= 1, "linesMax must be at least 1");
- memoryZero(box, sizeof(uitextbox_t));
- box->text = text;
- box->maxLength = maxLength;
- box->lines = lines;
- box->linesMax = linesMax;
-}
-
-void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
- assertNotNull(box, "Textbox cannot be NULL");
- assertNotNull(text, "Text cannot be NULL");
- stringCopy(box->text, text, box->maxLength);
- box->currentPage = 0;
- box->scroll = 0;
- box->layoutWidth = 0.0f;
- box->layoutHeight = 0.0f;
-}
-
-void uiTextboxBuildLayout(
- uitextbox_t *box,
- const float_t width,
- const float_t height
-) {
- assertNotNull(box, "Textbox cannot be NULL");
-
- box->layoutWidth = width;
- box->layoutHeight = height;
- box->lineCount = 0;
- box->pageCount = 1;
-
- float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
- float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
-
- if(fontW <= 0.0f || fontH <= 0.0f) return;
-
- box->charsPerLine = (int32_t)(width / fontW);
- box->linesPerPage = (int32_t)(height / (fontH + UI_TEXTBOX_LINE_SPACING));
- if(box->linesPerPage > UI_TEXTBOX_LINES_PER_PAGE_MAX) {
- box->linesPerPage = UI_TEXTBOX_LINES_PER_PAGE_MAX;
- }
-
- if(box->charsPerLine <= 0 || box->linesPerPage <= 0) return;
- if(box->text[0] == '\0') return;
-
- char_t *src = box->text;
- int32_t i = 0;
-
- while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) {
- if(src[i] == '\t') {
- i++;
- int32_t rem = box->lineCount % box->linesPerPage;
- int32_t pad = rem > 0 ? box->linesPerPage - rem : 0;
- while(pad > 0 && box->lineCount < (int32_t)box->linesMax) {
- box->lines[box->lineCount].start = i;
- box->lines[box->lineCount].count = 0;
- box->lineCount++;
- pad--;
- }
- continue;
- }
-
- int32_t lineStart = i;
- int32_t lineWidth = 0;
-
- while(src[i] != '\0') {
- char_t c = src[i];
-
- if(c == '\n') { i++; break; }
- if(c == '\t') break;
-
- if(c == ' ') {
- int32_t wordLen = 0;
- int32_t j = i + 1;
- while(
- src[j] != ' ' && src[j] != '\n' &&
- src[j] != '\t' && src[j] != '\0'
- ) {
- wordLen++;
- j++;
- }
-
- if(lineWidth > 0 && lineWidth + 1 + wordLen > box->charsPerLine) {
- i++;
- break;
- }
-
- lineWidth++;
- i++;
- } else {
- if(lineWidth >= box->charsPerLine) break;
- lineWidth++;
- i++;
- }
- }
-
- box->lines[box->lineCount].start = lineStart;
- box->lines[box->lineCount].count = lineWidth;
- box->lineCount++;
- }
-
- if(box->lineCount == 0) {
- box->pageCount = 1;
- } else {
- box->pageCount =
- (box->lineCount + box->linesPerPage - 1) / box->linesPerPage;
- }
-}
-
-errorret_t uiTextboxUpdate(uitextbox_t *box) {
- assertNotNull(box, "Textbox cannot be NULL");
-
-#ifdef DUSK_TIME_DYNAMIC
- if(TIME.dynamicUpdate) errorOk();
-#endif
-
- if(!uiTextboxPageIsComplete(box)) {
- box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK;
- }
-
- errorOk();
-}
-
-errorret_t uiTextboxDraw(
- uitextbox_t *box,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-) {
- assertNotNull(box, "Textbox cannot be NULL");
-
- float_t startX = (float_t)UI_FRAME_START_X;
- float_t startY = (float_t)UI_FRAME_START_Y;
- float_t contentX = x + startX;
- float_t contentY = y + startY;
- float_t contentW = width - 2.0f * startX;
- float_t contentH = height - 2.0f * startY;
-
- if(contentW != box->layoutWidth || contentH != box->layoutHeight) {
- uiTextboxBuildLayout(box, contentW, contentH);
- }
-
- errorChain(uiFrameDraw(x, y, width, height));
- errorChain(spriteBatchFlush());
-
- if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
-
- float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
- float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
-
- shadermaterial_t material = {
- .unlit = {
- .color = COLOR_WHITE,
- .texture = FONT_DEFAULT.texture
- }
- };
-
- int32_t pageFirst = box->currentPage * box->linesPerPage;
- int32_t pageLast = pageFirst + box->linesPerPage;
- if(pageLast > box->lineCount) pageLast = box->lineCount;
-
- int32_t charsLeft = box->scroll;
-
- for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
- uitextboxline_t *line = &box->lines[li];
- int32_t visible = line->count < charsLeft ? line->count : charsLeft;
- float_t lineY = contentY +
- (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
-
- for(int32_t ci = 0; ci < visible; ci++) {
- char_t c = box->text[line->start + ci];
- if(c == ' ') continue;
- spritebatchsprite_t sprite = textGetSprite(
- (vec2){ contentX + (float_t)ci * fontW, lineY },
- c,
- &FONT_DEFAULT
- );
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- }
-
- charsLeft -= visible;
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
- assertNotNull(box, "Textbox cannot be NULL");
- int32_t first = box->currentPage * box->linesPerPage;
- int32_t last = first + box->linesPerPage;
- if(last > box->lineCount) last = box->lineCount;
- int32_t total = 0;
- for(int32_t i = first; i < last; i++) {
- total += box->lines[i].count;
- }
- return total;
-}
-
-bool_t uiTextboxPageIsComplete(const uitextbox_t *box) {
- assertNotNull(box, "Textbox cannot be NULL");
- return box->scroll >= uiTextboxGetPageCharCount(box);
-}
-
-bool_t uiTextboxHasNextPage(const uitextbox_t *box) {
- assertNotNull(box, "Textbox cannot be NULL");
- return box->currentPage + 1 < box->pageCount;
-}
-
-void uiTextboxNextPage(uitextbox_t *box) {
- assertNotNull(box, "Textbox cannot be NULL");
- if(!uiTextboxHasNextPage(box)) return;
- box->currentPage++;
- box->scroll = 0;
-}
diff --git a/src/dusk/ui/rpg/textbox/uitextbox.h b/src/dusk/ui/rpg/textbox/uitextbox.h
deleted file mode 100644
index 047e7e1f..00000000
--- a/src/dusk/ui/rpg/textbox/uitextbox.h
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
-#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
-#define UI_TEXTBOX_LINE_SPACING 0.0f
-
-typedef struct {
- int32_t start;
- int32_t count;
-} uitextboxline_t;
-
-typedef struct {
- char_t *text;
- uint32_t maxLength;
-
- uitextboxline_t *lines;
- uint32_t linesMax;
- int32_t lineCount;
- int32_t charsPerLine;
- int32_t linesPerPage;
- int32_t pageCount;
-
- // last dimensions used for layout; rebuild triggers when these change
- float_t layoutWidth;
- float_t layoutHeight;
-
- int32_t currentPage;
- int32_t scroll;
-} uitextbox_t;
-
-/**
- * Initializes a textbox, zeroing all state and binding it to caller-owned
- * text and line storage.
- *
- * @param box The textbox to initialize.
- * @param text Caller-owned buffer the textbox copies its text into.
- * @param maxLength Capacity of text, in characters.
- * @param lines Caller-owned buffer the textbox lays lines out into.
- * @param linesMax Capacity of lines, in entries.
- */
-void uiTextboxInit(
- uitextbox_t *box,
- char_t *text,
- const uint32_t maxLength,
- uitextboxline_t *lines,
- const uint32_t linesMax
-);
-
-/**
- * Copies text into the textbox and marks layout as dirty.
- * Resets currentPage and scroll to 0.
- *
- * @param box The textbox to update.
- * @param text Null-terminated source string.
- */
-void uiTextboxSetText(uitextbox_t *box, const char_t *text);
-
-/**
- * Rebuilds word-wrap and page layout for the given draw dimensions.
- * Called automatically by uiTextboxDraw when width or height changes.
- *
- * @param box The textbox to rebuild.
- * @param width Available content width in pixels.
- * @param height Available content height in pixels.
- */
-void uiTextboxBuildLayout(
- uitextbox_t *box,
- const float_t width,
- const float_t height
-);
-
-/**
- * Advances the typewriter scroll by UI_TEXTBOX_SCROLL_CHARS_PER_TICK.
- * Skipped on dynamic ticks.
- *
- * @param box The textbox to update.
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxUpdate(uitextbox_t *box);
-
-/**
- * Draws the textbox frame and visible text. Rebuilds layout automatically
- * if width or height differs from the last draw call.
- *
- * @param box The textbox to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @param width Draw width in pixels.
- * @param height Draw height in pixels.
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxDraw(
- uitextbox_t *box,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-);
-
-/**
- * Returns the total visible char count for the current page.
- *
- * @param box The textbox to query.
- * @returns Total chars on the current page.
- */
-int32_t uiTextboxGetPageCharCount(const uitextbox_t *box);
-
-/**
- * Returns true when scroll has fully revealed the current page.
- *
- * @param box The textbox to query.
- * @returns True if the current page is fully visible.
- */
-bool_t uiTextboxPageIsComplete(const uitextbox_t *box);
-
-/**
- * Returns true when there is at least one more page after the current one.
- *
- * @param box The textbox to query.
- * @returns True if a next page exists.
- */
-bool_t uiTextboxHasNextPage(const uitextbox_t *box);
-
-/**
- * Advances to the next page and resets scroll to 0.
- * Has no effect if already on the last page.
- *
- * @param box The textbox to advance.
- */
-void uiTextboxNextPage(uitextbox_t *box);
diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.c b/src/dusk/ui/rpg/textbox/uitextboxmain.c
deleted file mode 100644
index 09918b1f..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxmain.c
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitextboxmain.h"
-#include "ui/focus/uifocus.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-#include "ui/widget/uiframe.h"
-
-uitextboxmain_t UI_TEXTBOX_MAIN;
-static uifocusitem_t *focusItem = NULL;
-
-errorret_t uiTextboxMainInit(void) {
- uiTextboxInit(
- &UI_TEXTBOX_MAIN.box,
- UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
- UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
- );
- errorOk();
-}
-
-void uiTextboxMainSetText(const char_t *text) {
- uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
- if(focusItem != NULL) return;
- focusItem = uiFocusPush(
- 1, 1,
- uiTextboxMainFocusSelected,
- NULL,
- uiTextboxMainFocusClosed,
- NULL,
- NULL
- );
-}
-
-errorret_t uiTextboxMainUpdate(void) {
- if(focusItem == NULL) errorOk();
- return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
-}
-
-errorret_t uiTextboxMainDraw(void) {
- if(focusItem == NULL) errorOk();
- float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
- float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
- (float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
- 2.0f * (float_t)UI_FRAME_START_Y;
- float_t w = (float_t)SCREEN.scanWidth;
- float_t x = (float_t)SCREEN.scanX;
- float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
- errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
-
- if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
-
- float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
- float_t contentX = x + (float_t)UI_FRAME_START_X;
- float_t contentY = y + (float_t)UI_FRAME_START_Y;
- float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
- float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
-
- shadermaterial_t material = {
- .unlit = {
- .color = COLOR_WHITE,
- .texture = FONT_DEFAULT.texture
- }
- };
-
- spritebatchsprite_t caret = textGetSprite(
- (vec2){
- contentX + contentW - fontW,
- contentY + contentH - fontH
- },
- 'v',
- &FONT_DEFAULT
- );
- errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
- errorChain(spriteBatchFlush());
-
- errorOk();
-}
-
-bool_t uiTextboxMainPageIsComplete(void) {
- return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
-}
-
-bool_t uiTextboxMainHasNextPage(void) {
- return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
-}
-
-void uiTextboxMainNextPage(void) {
- uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
-}
-
-bool_t uiTextboxMainIsActive(void) {
- return focusItem != NULL;
-}
-
-bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
- if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
- UI_TEXTBOX_MAIN.box.scroll =
- uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
- return true;
- }
-
- if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
- uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
- return true;
- }
-
- uiFocusPopItem(focusItem);
- return true;
-}
-
-bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
- focusItem = NULL;
- return true;
-}
diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.h b/src/dusk/ui/rpg/textbox/uitextboxmain.h
deleted file mode 100644
index e7eb83f6..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxmain.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "ui/rpg/textbox/uitextbox.h"
-#include "ui/focus/uifocusitem.h"
-
-#define UI_TEXTBOX_MAIN_LINES 4
-#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
-#define UI_TEXTBOX_MAIN_LINES_MAX 64
-
-typedef struct {
- uitextbox_t box;
- char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
- uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
-} uitextboxmain_t;
-
-extern uitextboxmain_t UI_TEXTBOX_MAIN;
-
-/**
- * Initializes UI_TEXTBOX_MAIN.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMainInit(void);
-
-/**
- * Copies text into UI_TEXTBOX_MAIN and resets page and scroll.
- *
- * @param text Null-terminated source string.
- */
-void uiTextboxMainSetText(const char_t *text);
-
-/**
- * Advances the typewriter scroll for UI_TEXTBOX_MAIN.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMainUpdate(void);
-
-/**
- * Draws UI_TEXTBOX_MAIN full-width at the bottom of the screen.
- * Position and size are derived from SCREEN each call.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMainDraw(void);
-
-/**
- * Returns true when the current page is fully scrolled in.
- *
- * @returns True if the current page is complete.
- */
-bool_t uiTextboxMainPageIsComplete(void);
-
-/**
- * Returns true when at least one more page follows the current one.
- *
- * @returns True if a next page exists.
- */
-bool_t uiTextboxMainHasNextPage(void);
-
-/**
- * Advances UI_TEXTBOX_MAIN to the next page and resets scroll.
- * Has no effect if already on the last page.
- */
-void uiTextboxMainNextPage(void);
-
-/**
- * Returns true when UI_TEXTBOX_MAIN has focus (is visible and active).
- *
- * @returns True if the textbox is currently active.
- */
-bool_t uiTextboxMainIsActive(void);
-
-/**
- * Internal focus callback - skip scroll or advance page or dismiss.
- *
- * @param item The active focus item.
- * @returns True.
- */
-bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item);
-
-/**
- * Internal focus callback - clears the focus item pointer on dismiss.
- *
- * @param item The focus item being closed.
- * @returns True.
- */
-bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item);
diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.c b/src/dusk/ui/rpg/textbox/uitextboxmini.c
deleted file mode 100644
index 8ae38f34..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxmini.c
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitextboxmini.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "rpg/rpgcamera.h"
-#include "display/text/text.h"
-#include "display/screen/screen.h"
-#include "ui/widget/uiframe.h"
-
-void uiTextboxMiniInit(uitextboxmini_t *mini) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- memoryZero(mini, sizeof(uitextboxmini_t));
- uiTextboxInit(
- &mini->box,
- mini->text, UI_TEXTBOX_MINI_TEXT_MAX,
- mini->lines, UI_TEXTBOX_MINI_LINES_MAX
- );
-}
-
-void uiTextboxMiniShow(
- uitextboxmini_t *mini,
- const char_t *text,
- const vec3 position,
- const float_t duration,
- uitextboxminiclosedcallback_t closed,
- void *user
-) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- assertNotNull(text, "Text cannot be NULL");
- uiTextboxSetText(&mini->box, text);
- glm_vec3_copy(position, mini->position);
-
- int32_t textWidth, textHeight;
- textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight);
- float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X;
- mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX
- ? width : UI_TEXTBOX_MINI_WIDTH_MAX;
-
- float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
- float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH +
- (float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING +
- 2.0f * (float_t)UI_FRAME_START_Y;
- uiTextboxBuildLayout(
- &mini->box,
- mini->width - 2.0f * (float_t)UI_FRAME_START_X,
- maxHeight - 2.0f * (float_t)UI_FRAME_START_Y
- );
- int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1;
- mini->height = (float_t)lineCount * fontH +
- (float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING +
- 2.0f * (float_t)UI_FRAME_START_Y;
-
- mini->active = true;
- mini->timer = duration;
- mini->closed = closed;
- mini->user = user;
-}
-
-errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- if(!mini->active) errorOk();
-
- errorChain(uiTextboxUpdate(&mini->box));
-
- if(mini->timer != UI_TEXTBOX_MINI_DURATION_INFINITE) {
- mini->timer -= TIME.delta;
- if(mini->timer <= 0.0f) uiTextboxMiniClose(mini);
- }
-
- errorOk();
-}
-
-errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- if(!mini->active) errorOk();
-
- vec2 screenPos;
- rpgCameraToScreen(mini->position, screenPos);
-
- if(
- mini->clamp == UI_TEXTBOX_MINI_CLAMP_X ||
- mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
- ) {
- float_t minX = (float_t)SCREEN.scanX;
- float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width;
- if(screenPos[0] < minX) screenPos[0] = minX;
- if(screenPos[0] > maxX) screenPos[0] = maxX;
- }
-
- if(
- mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y ||
- mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH
- ) {
- float_t minY = (float_t)SCREEN.scanY;
- float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height;
- if(screenPos[1] < minY) screenPos[1] = minY;
- if(screenPos[1] > maxY) screenPos[1] = maxY;
- }
-
- return uiTextboxDraw(
- &mini->box, screenPos[0], screenPos[1], mini->width, mini->height
- );
-}
-
-bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- return mini->active;
-}
-
-void uiTextboxMiniClose(uitextboxmini_t *mini) {
- assertNotNull(mini, "Mini textbox cannot be NULL");
- if(!mini->active) return;
- mini->active = false;
- if(mini->closed != NULL) mini->closed(mini);
-}
diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.h b/src/dusk/ui/rpg/textbox/uitextboxmini.h
deleted file mode 100644
index 16cf3f4b..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxmini.h
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "ui/rpg/textbox/uitextbox.h"
-
-#define UI_TEXTBOX_MINI_TEXT_MAX 128
-#define UI_TEXTBOX_MINI_LINES_MAX 2
-#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f
-
-// Magic duration value that keeps a mini textbox visible indefinitely
-// instead of counting down and auto-closing.
-#define UI_TEXTBOX_MINI_DURATION_INFINITE -1.0f
-
-typedef struct uitextboxmini_s uitextboxmini_t;
-typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini);
-
-/**
- * Determines whether a mini textbox's screen position is clamped to stay
- * fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE.
- */
-typedef enum {
- UI_TEXTBOX_MINI_CLAMP_NONE,
- UI_TEXTBOX_MINI_CLAMP_X,
- UI_TEXTBOX_MINI_CLAMP_Y,
- UI_TEXTBOX_MINI_CLAMP_BOTH
-} uitextboxminiclamp_t;
-
-typedef struct uitextboxmini_s {
- uitextbox_t box;
- char_t text[UI_TEXTBOX_MINI_TEXT_MAX];
- uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX];
-
- vec3 position;
- float_t width;
- float_t height;
- uitextboxminiclamp_t clamp;
- bool_t active;
- float_t timer;
-
- uitextboxminiclosedcallback_t closed;
- void *user;
-} uitextboxmini_t;
-
-/**
- * Initializes a mini textbox, zeroing all state and binding its internal
- * uitextbox_t to its own fixed-size text and line storage.
- *
- * @param mini The mini textbox to initialize.
- */
-void uiTextboxMiniInit(uitextboxmini_t *mini);
-
-/**
- * Shows a mini textbox with the given text at the given world position for
- * the given duration. Resets the typewriter scroll, measures the text to
- * size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived
- * from the resulting wrapped line count), and starts the visibility timer.
- *
- * @param mini The mini textbox to show.
- * @param text Null-terminated source string.
- * @param position World-space position the box is anchored to on screen.
- * @param duration How long the mini textbox stays visible, in seconds, or
- * UI_TEXTBOX_MINI_DURATION_INFINITE to never auto-close.
- * @param closed Called once the timer elapses and the box closes. May be
- * NULL.
- * @param user Opaque pointer passed back through the closed callback.
- */
-void uiTextboxMiniShow(
- uitextboxmini_t *mini,
- const char_t *text,
- const vec3 position,
- const float_t duration,
- uitextboxminiclosedcallback_t closed,
- void *user
-);
-
-/**
- * Advances the typewriter scroll and counts down the visibility timer.
- * Closes the mini textbox and fires its closed callback once the timer
- * elapses. Has no effect if not active or if the duration was set to
- * UI_TEXTBOX_MINI_DURATION_INFINITE.
- *
- * @param mini The mini textbox to update.
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini);
-
-/**
- * Draws the mini textbox at its text-measured size, anchored on screen to
- * its world-space position via the RPG camera. Has no effect if not
- * active.
- *
- * @param mini The mini textbox to draw.
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini);
-
-/**
- * Returns true when the mini textbox is currently visible.
- *
- * @param mini The mini textbox to query.
- * @returns True if active.
- */
-bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini);
-
-/**
- * Immediately closes the mini textbox and fires its closed callback.
- * Has no effect if not active.
- *
- * @param mini The mini textbox to close.
- */
-void uiTextboxMiniClose(uitextboxmini_t *mini);
diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.c b/src/dusk/ui/rpg/textbox/uitextboxminilist.c
deleted file mode 100644
index 070bc817..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxminilist.c
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitextboxminilist.h"
-
-uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
-uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
-
-errorret_t uiTextboxMiniListInit(void) {
- for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
- uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]);
- }
- UI_TEXTBOX_MINI_LIST_NEXT = 0;
- errorOk();
-}
-
-uint8_t uiTextboxMiniListGetNext(void) {
- uint8_t index = UI_TEXTBOX_MINI_LIST_NEXT;
- UI_TEXTBOX_MINI_LIST_NEXT =
- (UI_TEXTBOX_MINI_LIST_NEXT + 1) % UI_TEXTBOX_MINI_LIST_COUNT;
- return index;
-}
-
-errorret_t uiTextboxMiniListUpdate(void) {
- for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
- errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i]));
- }
- errorOk();
-}
-
-errorret_t uiTextboxMiniListDraw(void) {
- for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) {
- errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i]));
- }
- errorOk();
-}
diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.h b/src/dusk/ui/rpg/textbox/uitextboxminilist.h
deleted file mode 100644
index f3040b2c..00000000
--- a/src/dusk/ui/rpg/textbox/uitextboxminilist.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "ui/rpg/textbox/uitextboxmini.h"
-
-#define UI_TEXTBOX_MINI_LIST_COUNT 4
-
-extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT];
-extern uint8_t UI_TEXTBOX_MINI_LIST_NEXT;
-
-/**
- * Initializes all UI_TEXTBOX_MINI_LIST slots and resets
- * UI_TEXTBOX_MINI_LIST_NEXT.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMiniListInit(void);
-
-/**
- * Returns the index of the next UI_TEXTBOX_MINI_LIST slot to use, cycling
- * through all slots in round-robin order via UI_TEXTBOX_MINI_LIST_NEXT.
- *
- * @returns The next slot index.
- */
-uint8_t uiTextboxMiniListGetNext(void);
-
-/**
- * Updates all UI_TEXTBOX_MINI_LIST slots.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMiniListUpdate(void);
-
-/**
- * Draws all active UI_TEXTBOX_MINI_LIST slots.
- *
- * @returns Any error that occurs.
- */
-errorret_t uiTextboxMiniListDraw(void);
diff --git a/src/dusk/ui/rpg/uiemoji.c b/src/dusk/ui/rpg/uiemoji.c
deleted file mode 100644
index 21a3cbf2..00000000
--- a/src/dusk/ui/rpg/uiemoji.c
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiemoji.h"
-#include "display/spritebatch/spritebatch.h"
-#include "time/time.h"
-#include "util/memory.h"
-#include "display/shader/shaderunlit.h"
-#include "rpg/entity/entity.h"
-#include "rpg/rpgcamera.h"
-
-uiemoji_t UI_EMOJI;
-
-errorret_t uiEmojiInit(void) {
- memoryZero(&UI_EMOJI, sizeof(UI_EMOJI));
- errorOk();
-}
-
-void uiEmojiAdd(
- const uint8_t entity,
- const float_t duration,
- const uiemojitype_t type
-) {
- uint8_t slot = 0xFF;
- // Find an available slot if any
- for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
- if(UI_EMOJI.items[i].time <= 0.0f) {
- slot = i;
- break;
- }
-
- if(UI_EMOJI.items[i].entity == entity) {
- slot = i;
- break;
- }
-
- if(UI_EMOJI.items[i].type == UI_EMOJI_NULL) {
- slot = i;
- break;
- }
- }
-
- if(slot == 0xFF) {
- // Whichever ends soonest
- float_t minTime = UI_EMOJI.items[0].time;
- slot = 0;
- for(uint8_t i = 1; i < UI_EMOJI_ACTIVE_COUNT; i++) {
- if(UI_EMOJI.items[i].time >= minTime) continue;
- minTime = UI_EMOJI.items[i].time;
- slot = i;
- }
- }
-
- uiemojiitem_t *item = &UI_EMOJI.items[slot];
- item->entity = entity;
- item->time = duration;
- item->type = type;
-}
-
-errorret_t uiEmojiUpdate(void) {
- for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
- uiemojiitem_t *item = &UI_EMOJI.items[i];
- if(item->type == UI_EMOJI_NULL) continue;
-
- item->time -= TIME.delta;
-
- if(item->time <= 0.0f) {
- item->type = UI_EMOJI_NULL;
- item->entity = 0xFF;
- }
- }
-
- errorOk();
-}
-
-errorret_t uiEmojiDraw(void) {
- spritebatchsprite_t sprites[UI_EMOJI_ACTIVE_COUNT];
- uint8_t count = 0;
-
- for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) {
- uiemojiitem_t *item = &UI_EMOJI.items[i];
- if(item->type == UI_EMOJI_NULL) continue;
-
- vec2 screenPos;
- rpgCameraToScreen(ENTITIES[item->entity].renderPosition, screenPos);
-
- spritebatchsprite_t *sprite = &sprites[count++];
- sprite->min[0] = screenPos[0];
- sprite->min[1] = screenPos[1] - UI_EMOJI_SIZE;
- sprite->min[2] = 0.0f;
- sprite->max[0] = screenPos[0] + UI_EMOJI_SIZE;
- sprite->max[1] = screenPos[1];
- sprite->max[2] = 0.0f;
- sprite->uvMin[0] = 0.0f;
- sprite->uvMin[1] = 0.0f;
- sprite->uvMax[0] = 1.0f;
- sprite->uvMax[1] = 1.0f;
- }
-
- if(count == 0) errorOk();
-
- shadermaterial_t mat = {
- .unlit = {
- .color = COLOR_WHITE,
- .texture = NULL
- }
- };
- errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, mat));
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-errorret_t uiEmojiDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/rpg/uiemoji.h b/src/dusk/ui/rpg/uiemoji.h
deleted file mode 100644
index 6dc8cc00..00000000
--- a/src/dusk/ui/rpg/uiemoji.h
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_EMOJI_ACTIVE_COUNT 4
-#define UI_EMOJI_SIZE 32.0f
-
-typedef enum {
- UI_EMOJI_NULL,
-
- UI_EMOJI_QUESTION_MARK,
- UI_EMOJI_EXCLAMATION_MARK,
-
- UI_EMOJI_COUNT,
-} uiemojitype_t;
-
-typedef struct {
- uint8_t entity;
- float_t time;
- uiemojitype_t type;
-} uiemojiitem_t;
-
-typedef struct {
- uiemojiitem_t items[UI_EMOJI_ACTIVE_COUNT];
-} uiemoji_t;
-
-extern uiemoji_t UI_EMOJI;
-
-/**
- * Initialize the UI emoji.
- */
-errorret_t uiEmojiInit(void);
-
-/**
- * Show a UI emoji for a specific entity.
- *
- * @param entity The entity to show the emoji for.
- * @param duration The duration to display the emoji.
- * @param type The type of emoji to display.
- */
-void uiEmojiAdd(
- const uint8_t entity,
- const float_t duration,
- const uiemojitype_t type
-);
-
-/**
- * Update the UI emoji.
- */
-errorret_t uiEmojiUpdate(void);
-
-/**
- * Draw the UI emoji.
- */
-errorret_t uiEmojiDraw(void);
-
-/**
- * Dispose of the UI emoji.
- */
-errorret_t uiEmojiDispose(void);
\ No newline at end of file
diff --git a/src/dusk/ui/screen/CMakeLists.txt b/src/dusk/ui/screen/CMakeLists.txt
deleted file mode 100644
index 6934dc3b..00000000
--- a/src/dusk/ui/screen/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-add_subdirectory(game)
-add_subdirectory(mainmenu)
-add_subdirectory(battle)
-add_subdirectory(backpack)
\ No newline at end of file
diff --git a/src/dusk/ui/screen/backpack/CMakeLists.txt b/src/dusk/ui/screen/backpack/CMakeLists.txt
deleted file mode 100644
index 570abb53..00000000
--- a/src/dusk/ui/screen/backpack/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uibackpack.c
-)
diff --git a/src/dusk/ui/screen/backpack/uibackpack.c b/src/dusk/ui/screen/backpack/uibackpack.c
deleted file mode 100644
index 269b8f0d..00000000
--- a/src/dusk/ui/screen/backpack/uibackpack.c
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uibackpack.h"
-#include "ui/widget/uiframe.h"
-#include "rpg/item/backpack.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "assert/assert.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-uibackpack_t UI_BACKPACK;
-
-void uiBackpackTabChanged(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- const itemtype_t type = (itemtype_t)(index + 1);
- const inventory_t *inventory = backpackGetInventory(type);
-
- errorCatch(uiItemListSetItemStacks(
- &UI_BACKPACK.itemList, inventory->storage, inventory->storageSize
- ));
-}
-
-void uiBackpackTabSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- uiItemListOpen(&UI_BACKPACK.itemList);
-}
-
-errorret_t uiBackpackInit(void) {
- memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
-
- errorChain(localeManagerGetString(
- "ui.backpack.category_format",
- UI_BACKPACK.categoryFormat,
- UI_BACKPACK_CATEGORY_FORMAT_MAX,
- 0
- ));
-
- MENU_BEGIN(
- &UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
- uiBackpackTabSelected, NULL, uiBackpackTabChanged
- );
- const uint8_t tabCount = ITEM.typeCount - 1;
- for(uint8_t i = 0; i < tabCount; i++) {
- stringFormat(
- UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
- UI_BACKPACK.categoryFormat, i + 1
- );
- MENU_TAB(UI_BACKPACK.tabLabels[i]);
- }
- MENU_END(UI_BACKPACK.tabs, menuIndex);
-
- uiItemListInit(
- &UI_BACKPACK.itemList,
- UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1,
- NULL, NULL, NULL, NULL
- );
-
- errorOk();
-}
-
-errorret_t uiBackpackDraw(void) {
- if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk();
-
- const float_t width = SCREEN.width;
- const float_t height = SCREEN.height;
- const float_t x = (float_t)SCREEN.scanX +
- ((float_t)SCREEN.scanWidth - width) * 0.5f;
- const float_t y = (float_t)SCREEN.scanY +
- ((float_t)SCREEN.scanHeight - height) * 0.5f;
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- const float_t contentX = x + UI_FRAME_START_X;
- const float_t contentY = y + UI_FRAME_START_Y;
- const float_t contentWidth = width - (UI_FRAME_START_X * 2);
- const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
-
- errorChain(uiMenuDraw(
- &UI_BACKPACK.tabsMenu,
- contentX,
- contentY,
- contentWidth,
- contentHeight
- ));
-
- const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
-
- errorChain(
- uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
- );
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-bool_t uiBackpackIsOpen(void) {
- return uiMenuIsActive(&UI_BACKPACK.tabsMenu);
-}
-
-void uiBackpackOpen(void) {
- uiMenuOpen(&UI_BACKPACK.tabsMenu);
-}
-
-void uiBackpackClose(void) {
- uiMenuClose(&UI_BACKPACK.tabsMenu);
-}
-
-errorret_t uiBackpackDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/screen/backpack/uibackpack.h b/src/dusk/ui/screen/backpack/uibackpack.h
deleted file mode 100644
index f48c076d..00000000
--- a/src/dusk/ui/screen/backpack/uibackpack.h
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uimenu.h"
-#include "ui/widget/uiitemlist.h"
-#include "rpg/item/item.h"
-
-#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_DEF_COUNT_MAX - 1)
-#define UI_BACKPACK_TAB_LABEL_MAX 32
-#define UI_BACKPACK_CATEGORY_FORMAT_MAX 32
-#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
-#define UI_BACKPACK_ITEM_LIST_ROWS 5
-
-typedef struct {
- uimenu_t tabsMenu;
- uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT];
- char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX];
- char_t categoryFormat[UI_BACKPACK_CATEGORY_FORMAT_MAX];
-
- uiitemlist_t itemList;
-} uibackpack_t;
-
-extern uibackpack_t UI_BACKPACK;
-
-/**
- * Initializes the backpack panel and its item type tabs.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBackpackInit(void);
-
-/**
- * Draws the backpack panel. No-op when not visible.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBackpackDraw(void);
-
-/**
- * Returns true when the backpack panel is currently open.
- *
- * @returns True if open.
- */
-bool_t uiBackpackIsOpen(void);
-
-/**
- * Opens the backpack panel. No-op when already open.
- */
-void uiBackpackOpen(void);
-
-/**
- * Closes the backpack panel. No-op when already closed.
- */
-void uiBackpackClose(void);
-
-/**
- * Disposes of the backpack panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBackpackDispose(void);
diff --git a/src/dusk/ui/screen/battle/CMakeLists.txt b/src/dusk/ui/screen/battle/CMakeLists.txt
deleted file mode 100644
index 1cdef8a0..00000000
--- a/src/dusk/ui/screen/battle/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uibattlemenu.c
- uibattlehud.c
-)
diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c
deleted file mode 100644
index 8793a0ac..00000000
--- a/src/dusk/ui/screen/battle/uibattlehud.c
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uibattlehud.h"
-#include "rpg/battle/battle.h"
-#include "rpg/battle/party.h"
-#include "scene/battle/scenebattle.h"
-#include "scene/scene.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "util/string.h"
-#include "util/memory.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-#define UI_BATTLE_HUD_TEXT_MAX 32
-#define UI_BATTLE_HUD_LINE_HEIGHT 12.0f
-
-uibattlehud_t UI_BATTLE_HUD;
-
-errorret_t uiBattleHudInit(void) {
- memoryZero(&UI_BATTLE_HUD, sizeof(uibattlehud_t));
-
- errorChain(localeManagerGetString(
- "ui.battle.hud.hp_format",
- UI_BATTLE_HUD.hpFormat,
- UI_BATTLE_HUD_FORMAT_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.battle.hud.mp_format",
- UI_BATTLE_HUD.mpFormat,
- UI_BATTLE_HUD_FORMAT_MAX,
- 0
- ));
-
- errorOk();
-}
-
-errorret_t uiBattleHudDraw(void) {
- if(BATTLE.state == BATTLE_STATE_NULL) errorOk();
-
- scenebattle_t *battle = &SCENE.data.battle;
-
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- const battlefighter_t *fighter = BATTLE.fighters[i];
- if(fighter == NULL) continue;
- if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue;
-
- vec3 position;
- sceneBattleGetFighterPosition(i, position);
- glm_vec3_add(position, (vec3){ 0.5f, 1.3f, 0.0f }, position);
-
- vec2 screenPos;
- sceneBattleWorldToScreen(battle, position, screenPos);
-
- float_t lineY = screenPos[1];
- if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) {
- errorChain(textDraw(
- screenPos[0], lineY, partyGetMemberKey(fighter->id), COLOR_WHITE, NULL
- ));
- lineY += UI_BATTLE_HUD_LINE_HEIGHT;
- }
-
- char_t hpText[UI_BATTLE_HUD_TEXT_MAX];
- stringFormat(
- hpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.hpFormat,
- fighter->stats.health, fighter->stats.healthMax
- );
- errorChain(textDraw(screenPos[0], lineY, hpText, COLOR_WHITE, NULL));
- lineY += UI_BATTLE_HUD_LINE_HEIGHT;
-
- char_t mpText[UI_BATTLE_HUD_TEXT_MAX];
- stringFormat(
- mpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.mpFormat,
- fighter->stats.mp, fighter->stats.mpMax
- );
- errorChain(textDraw(screenPos[0], lineY, mpText, COLOR_CYAN, NULL));
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
diff --git a/src/dusk/ui/screen/battle/uibattlehud.h b/src/dusk/ui/screen/battle/uibattlehud.h
deleted file mode 100644
index 47c38f17..00000000
--- a/src/dusk/ui/screen/battle/uibattlehud.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_BATTLE_HUD_FORMAT_MAX 32
-
-typedef struct {
- char_t hpFormat[UI_BATTLE_HUD_FORMAT_MAX];
- char_t mpFormat[UI_BATTLE_HUD_FORMAT_MAX];
-} uibattlehud_t;
-
-extern uibattlehud_t UI_BATTLE_HUD;
-
-/**
- * Loads the HP/MP format strings from the active locale.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBattleHudInit(void);
-
-/**
- * Draws each living battle fighter's HP/MP as text above its position in
- * the battle scene. No-op if no battle is active.
- *
- * @return An error if drawing failed, or errorOk() if it succeeded.
- */
-errorret_t uiBattleHudDraw(void);
diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c
deleted file mode 100644
index bc7bc077..00000000
--- a/src/dusk/ui/screen/battle/uibattlemenu.c
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uibattlemenu.h"
-#include "ui/widget/uiframe.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-#include "rpg/battle/party.h"
-
-uibattlemenu_t UI_BATTLE_MENU;
-
-void uiBattleMenuTargetSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- assertTrue(index < UI_BATTLE_MENU_TARGET_ITEM_COUNT, "index out of bounds");
- battleStateSelectionPlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
- uiMenuClose(&UI_BATTLE_MENU.targetMenu);
- uiMenuClose(&UI_BATTLE_MENU.actionMenu);
-}
-
-void uiBattleMenuOpenTargets(void) {
- battlefighter_t *current = battleStateSelectionGetCurrentFighter();
- if(current == NULL) return;
-
- const battlefighterteam_t enemyTeam =
- current->team == BATTLE_FIGHTER_TEAM_ALLY ?
- BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
-
- MENU_BEGIN(
- &UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems,
- uiBattleMenuTargetSelected, NULL, NULL
- );
-
- for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
- battlefighter_t *candidate = BATTLE.fighters[i];
- if(candidate == NULL) continue;
- if(candidate->team != enemyTeam) continue;
- if(!battleFighterIsAlive(candidate)) continue;
-
- stringFormat(
- UI_BATTLE_MENU.targetLabels[menuIndex],
- UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
- UI_BATTLE_MENU.targetFormat,
- candidate->id, candidate->stats.health, candidate->stats.healthMax
- );
- UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
- MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
- }
-
- MENU_END(UI_BATTLE_MENU.targetItems, 1);
-
- uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
-}
-
-bool_t uiBattleMenuActionCancel(const uimenu_t *menu) {
- battleStateSelectionGoBack();
- return true;
-}
-
-void uiBattleMenuActionSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- switch(index) {
- case UI_BATTLE_MENU_ACTION_INDEX_ATTACK:
- uiBattleMenuOpenTargets();
- break;
-
- case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
- battleStateSelectionPlayerFlee();
- uiMenuClose(&UI_BATTLE_MENU.actionMenu);
- break;
-
- default:
- break;
- }
-}
-
-errorret_t uiBattleMenuInit(void) {
- memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t));
-
- errorChain(localeManagerGetString(
- "ui.battle.menu.attack",
- UI_BATTLE_MENU.attackLabel,
- UI_BATTLE_MENU_ACTION_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.battle.menu.flee",
- UI_BATTLE_MENU.fleeLabel,
- UI_BATTLE_MENU_ACTION_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.battle.menu.target_format",
- UI_BATTLE_MENU.targetFormat,
- UI_BATTLE_MENU_TARGET_FORMAT_MAX,
- 0
- ));
-
- MENU_BEGIN(
- &UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems,
- uiBattleMenuActionSelected, NULL, NULL
- );
- MENU_BUTTON(UI_BATTLE_MENU.attackLabel);
- MENU_BUTTON(UI_BATTLE_MENU.fleeLabel);
- MENU_END(UI_BATTLE_MENU.actionItems, 1);
-
- uiMenuSetCancelCallback(&UI_BATTLE_MENU.actionMenu, uiBattleMenuActionCancel);
-
- errorOk();
-}
-
-errorret_t uiBattleMenuUpdate(void) {
- if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
-
- battlefighter_t *current = battleStateSelectionGetCurrentFighter();
- if(current == NULL) errorOk();
- if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();
-
- uiMenuOpen(&UI_BATTLE_MENU.actionMenu);
- errorOk();
-}
-
-errorret_t uiBattleMenuDraw(void) {
- if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
-
- const float_t width = UI_BATTLE_MENU_WIDTH;
- const float_t height = UI_BATTLE_MENU_HEIGHT;
- const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
- const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height;
-
- battlefighter_t *current = battleStateSelectionGetCurrentFighter();
- if(current != NULL) {
- errorChain(textDraw(
- x, y - UI_BATTLE_MENU_NAME_LINE_HEIGHT,
- partyGetMemberKey(current->id), COLOR_WHITE, NULL
- ));
- }
-
- errorChain(uiFrameDraw(x, y, width, height));
- errorChain(uiMenuDraw(
- &UI_BATTLE_MENU.actionMenu,
- x + UI_FRAME_START_X,
- y + UI_FRAME_START_Y,
- width - (UI_FRAME_START_X * 2),
- height - (UI_FRAME_START_Y * 2)
- ));
-
- if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) {
- const float_t targetY = y - height;
- errorChain(uiFrameDraw(x, targetY, width, height));
- errorChain(uiMenuDraw(
- &UI_BATTLE_MENU.targetMenu,
- x + UI_FRAME_START_X,
- targetY + UI_FRAME_START_Y,
- width - (UI_FRAME_START_X * 2),
- height - (UI_FRAME_START_Y * 2)
- ));
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
diff --git a/src/dusk/ui/screen/battle/uibattlemenu.h b/src/dusk/ui/screen/battle/uibattlemenu.h
deleted file mode 100644
index 6d531a74..00000000
--- a/src/dusk/ui/screen/battle/uibattlemenu.h
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uimenu.h"
-#include "rpg/battle/battle.h"
-
-#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2
-#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0
-#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
-#define UI_BATTLE_MENU_ACTION_LABEL_MAX 32
-
-#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_POS_COUNT
-#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
-#define UI_BATTLE_MENU_TARGET_FORMAT_MAX 48
-
-#define UI_BATTLE_MENU_WIDTH 160.0f
-#define UI_BATTLE_MENU_HEIGHT 96.0f
-#define UI_BATTLE_MENU_NAME_LINE_HEIGHT 12.0f
-
-typedef struct {
- uimenu_t actionMenu;
- uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT];
- char_t attackLabel[UI_BATTLE_MENU_ACTION_LABEL_MAX];
- char_t fleeLabel[UI_BATTLE_MENU_ACTION_LABEL_MAX];
-
- uimenu_t targetMenu;
- uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
- uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
- char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT]
- [UI_BATTLE_MENU_TARGET_LABEL_MAX];
- char_t targetFormat[UI_BATTLE_MENU_TARGET_FORMAT_MAX];
-} uibattlemenu_t;
-
-extern uibattlemenu_t UI_BATTLE_MENU;
-
-/**
- * Initializes the battle action/target menus.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBattleMenuInit(void);
-
-/**
- * Updates the battle menu: opens the action menu whenever it becomes a
- * player-controlled fighter's turn.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBattleMenuUpdate(void);
-
-/**
- * Draws the battle action menu, and the target menu above it when
- * open. No-op when the action menu isn't active.
- *
- * @return Any error that occurs.
- */
-errorret_t uiBattleMenuDraw(void);
diff --git a/src/dusk/ui/screen/game/CMakeLists.txt b/src/dusk/ui/screen/game/CMakeLists.txt
deleted file mode 100644
index 4b79eda6..00000000
--- a/src/dusk/ui/screen/game/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uigamemenu.c
-)
diff --git a/src/dusk/ui/screen/game/uigamemenu.c b/src/dusk/ui/screen/game/uigamemenu.c
deleted file mode 100644
index dbe26561..00000000
--- a/src/dusk/ui/screen/game/uigamemenu.c
+++ /dev/null
@@ -1,152 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uigamemenu.h"
-#include "ui/widget/uiframe.h"
-#include "ui/dialog/uiconfirm.h"
-#include "ui/screen/backpack/uibackpack.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "util/memory.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "assert/assert.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-#include "error/error.h"
-#include "util/string.h"
-
-#define UI_GAME_MENU_INDEX_CHARACTERS 0
-#define UI_GAME_MENU_INDEX_ITEMS 1
-
-uigamemenu_t UI_GAME_MENU;
-
-void uiGameMenuSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- switch(index) {
- case UI_GAME_MENU_INDEX_ITEMS:
- uiBackpackOpen();
- break;
- default:
- assertUnreachable("Unhandled game menu index");
- }
-}
-
-errorret_t uiGameMenuInit(void) {
- memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
-
- errorChain(localeManagerGetString(
- "ui.game_menu.characters",
- UI_GAME_MENU.charactersLabel,
- UI_GAME_MENU_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.items",
- UI_GAME_MENU.itemsLabel,
- UI_GAME_MENU_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.settings",
- UI_GAME_MENU.settingsLabel,
- UI_GAME_MENU_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_success",
- UI_GAME_MENU.saveSuccessText,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_cancelled",
- UI_GAME_MENU.saveCancelledText,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_unavailable",
- UI_GAME_MENU.saveUnavailableText,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_temporary",
- UI_GAME_MENU.saveTemporaryText,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_create_confirm",
- UI_GAME_MENU.saveCreateConfirmText,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_failed_format",
- UI_GAME_MENU.saveFailedFormat,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "ui.game_menu.save_check_failed_format",
- UI_GAME_MENU.saveCheckFailedFormat,
- UI_GAME_MENU_MESSAGE_MAX,
- 0
- ));
-
- MENU_BEGIN(
- &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
- );
- MENU_BUTTON(UI_GAME_MENU.charactersLabel);
- MENU_BUTTON(UI_GAME_MENU.itemsLabel);
- MENU_BUTTON(UI_GAME_MENU.saveLabel);
-
- MENU_END(UI_GAME_MENU.items, 1);
-
- errorOk();
-}
-
-errorret_t uiGameMenuDraw(void) {
- if(!uiMenuIsActive(&UI_GAME_MENU.menu)) errorOk();
-
- const float_t width = UI_GAME_MENU_WIDTH;
- const float_t height = (float_t)SCREEN.scanHeight;
- const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
- const float_t y = (float_t)SCREEN.scanY;
-
- errorChain(uiFrameDraw(x, y, width, height));
- errorChain(uiMenuDraw(
- &UI_GAME_MENU.menu,
- x + UI_FRAME_START_X,
- y + UI_FRAME_START_Y,
- width - (UI_FRAME_START_X * 2),
- height - (UI_FRAME_START_Y * 2)
- ));
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-bool_t uiGameMenuIsOpen(void) {
- return uiMenuIsActive(&UI_GAME_MENU.menu);
-}
-
-void uiGameMenuOpen() {
- uiMenuOpen(&UI_GAME_MENU.menu);
-}
-
-void uiGameMenuClose() {
- uiMenuClose(&UI_GAME_MENU.menu);
-}
-
-errorret_t uiGameMenuDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/screen/game/uigamemenu.h b/src/dusk/ui/screen/game/uigamemenu.h
deleted file mode 100644
index 87ec2a4e..00000000
--- a/src/dusk/ui/screen/game/uigamemenu.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uimenu.h"
-
-#define UI_GAME_MENU_ITEM_COUNT 4
-#define UI_GAME_MENU_WIDTH 150.0f
-#define UI_GAME_MENU_LABEL_MAX 32
-#define UI_GAME_MENU_MESSAGE_MAX 192
-
-typedef struct {
- uimenu_t menu;
- uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT];
- char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
- char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
- char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
- char_t saveLabel[UI_GAME_MENU_LABEL_MAX];
- char_t saveSuccessText[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveCancelledText[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveUnavailableText[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveTemporaryText[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveCreateConfirmText[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveFailedFormat[UI_GAME_MENU_MESSAGE_MAX];
- char_t saveCheckFailedFormat[UI_GAME_MENU_MESSAGE_MAX];
-} uigamemenu_t;
-
-extern uigamemenu_t UI_GAME_MENU;
-
-/**
- * Initializes the game menu panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiGameMenuInit(void);
-
-/**
- * Draws the game menu panel. No-op when not visible.
- *
- * @return Any error that occurs.
- */
-errorret_t uiGameMenuDraw(void);
-
-/**
- * Returns true when the game menu panel is currently open.
- *
- * @returns True if open.
- */
-bool_t uiGameMenuIsOpen(void);
-
-/**
- * Opens the game menu panel. No-op when already open.
- */
-void uiGameMenuOpen();
-
-/**
- * Closes the game menu panel. No-op when already closed.
- */
-void uiGameMenuClose();
-
-/**
- * Disposes of the game menu panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiGameMenuDispose(void);
diff --git a/src/dusk/ui/screen/mainmenu/CMakeLists.txt b/src/dusk/ui/screen/mainmenu/CMakeLists.txt
deleted file mode 100644
index 2231cb11..00000000
--- a/src/dusk/ui/screen/mainmenu/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uimainmenu.c
-)
diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c
deleted file mode 100644
index 168a1d63..00000000
--- a/src/dusk/ui/screen/mainmenu/uimainmenu.c
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uimainmenu.h"
-#include "ui/widget/uiframe.h"
-#include "util/memory.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "assert/assert.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-#include "ui/dialog/save/uiselectsave.h"
-#include "ui/overlay/uifatalerror.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "scene/scene.h"
-#include "asset/asset.h"
-#include "save/save.h"
-#include "rpg/overworld/map.h"
-
-#define UI_MAIN_MENU_INDEX_START_GAME 0
-#define UI_MAIN_MENU_INDEX_OPTIONS 1
-#define UI_MAIN_MENU_INDEX_QUIT 2
-
-uimainmenu_t UI_MAIN_MENU;
-
-cutscene_t *uiMainMenuLoadCutscene(void) {
- return cutsceneCutsceneResolve("main_menu");
-}
-
-void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
- if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) {
- cutsceneSystemLoad("cutscenes/main_menu.jsonc");
- return;
- }
-
- SAVE.slotCurrent = slotIndex;
- errorret_t initResult = saveSlotInit(&SAVE.slot);
- if(errorIsNotOk(initResult)) {
- errorCatch(errorPrint(initResult));
- uiFatalErrorOpen(initResult.state->message);
- return;
- }
-
- errorret_t result = saveLoadSlot();
- if(errorIsNotOk(result)) {
- errorCatch(errorPrint(result));
- uiFatalErrorOpen(result.state->message);
- return;
- }
-
- assertStrLenMin(
- SAVE.slot.cachedData.mapName, 1, "Save slot has no map name"
- );
- errorret_t mapResult = mapSetMap(SAVE.slot.cachedData.mapName);
- if(errorIsNotOk(mapResult)) {
- errorCatch(errorPrint(mapResult));
- uiFatalErrorOpen(mapResult.state->message);
- return;
- }
-
- sceneSet(SCENE_TYPE_OVERWORLD);
-
- if(SAVE.slot.currentCutscene[0] != '\0') {
- cutsceneSystemLoad(SAVE.slot.currentCutscene);
- }
-}
-
-void uiMainMenuOpenSelectSave(void *userData) {
- uiSelectSaveOpen(
- UI_SELECT_SAVE_TYPE_LOAD, uiMainMenuSelectSaveResult, NULL
- );
-}
-
-void uiMainMenuStartGame(void) {
- if(CUTSCENE_SYSTEM.scene == NULL) {
- cutscene_t *cutscene = uiMainMenuLoadCutscene();
- if(cutscene == NULL) return;
- cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "NEW_GAME");
- } else {
- cutsceneSystemGoTo("NEW_GAME");
- }
-
- cutsceneSystemSetOnComplete(uiMainMenuOpenSelectSave);
-}
-
-void uiMainMenuOptions(void) {
- if(CUTSCENE_SYSTEM.scene == NULL) {
- cutscene_t *cutscene = uiMainMenuLoadCutscene();
- if(cutscene == NULL) return;
- cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "OPTIONS");
- } else {
- cutsceneSystemGoTo("OPTIONS");
- }
-}
-
-void uiMainMenuQuit(void) {
- if(CUTSCENE_SYSTEM.scene == NULL) {
- cutscene_t *cutscene = uiMainMenuLoadCutscene();
- if(cutscene == NULL) return;
- cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "QUIT");
- } else {
- cutsceneSystemGoTo("QUIT");
- }
-}
-
-void uiMainMenuSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
-
- switch(index) {
- case UI_MAIN_MENU_INDEX_START_GAME:
- uiMenuClose(&UI_MAIN_MENU.menu);
- uiMainMenuStartGame();
- break;
-
- case UI_MAIN_MENU_INDEX_OPTIONS:
- uiMainMenuOptions();
- break;
-
- case UI_MAIN_MENU_INDEX_QUIT:
- uiMenuClose(&UI_MAIN_MENU.menu);
- uiMainMenuQuit();
- break;
-
- default:
- break;
- }
-}
-
-errorret_t uiMainMenuInit(void) {
- memoryZero(&UI_MAIN_MENU, sizeof(uimainmenu_t));
-
- errorChain(localeManagerGetString(
- "main_menu.start_game",
- UI_MAIN_MENU.startGameLabel,
- UI_MAIN_MENU_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "main_menu.options",
- UI_MAIN_MENU.optionsLabel,
- UI_MAIN_MENU_LABEL_MAX,
- 0
- ));
- errorChain(localeManagerGetString(
- "main_menu.quit",
- UI_MAIN_MENU.quitLabel,
- UI_MAIN_MENU_LABEL_MAX,
- 0
- ));
-
- MENU_BEGIN(
- &UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
- );
- MENU_BUTTON(UI_MAIN_MENU.startGameLabel);
- MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
- MENU_BUTTON(UI_MAIN_MENU.quitLabel);
- MENU_END(UI_MAIN_MENU.items, 1);
- uiMenuSetDisableBack(&UI_MAIN_MENU.menu, true);
-
- errorOk();
-}
-
-void uiMainMenuOpen(void) {
- if(uiMenuIsActive(&UI_MAIN_MENU.menu)) return;
- uiMenuOpen(&UI_MAIN_MENU.menu);
-}
-
-errorret_t uiMainMenuDraw(void) {
- if(!uiMenuIsActive(&UI_MAIN_MENU.menu)) errorOk();
-
- const float_t width = UI_MAIN_MENU_WIDTH;
- const float_t height = UI_MAIN_MENU_HEIGHT;
- const float_t x =
- (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
- const float_t y =
- (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
-
- errorChain(uiFrameDraw(x, y, width, height));
- errorChain(uiMenuDraw(
- &UI_MAIN_MENU.menu,
- x + UI_FRAME_START_X,
- y + UI_FRAME_START_Y,
- width - (UI_FRAME_START_X * 2),
- height - (UI_FRAME_START_Y * 2)
- ));
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-errorret_t uiMainMenuDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.h b/src/dusk/ui/screen/mainmenu/uimainmenu.h
deleted file mode 100644
index 82f9b9cb..00000000
--- a/src/dusk/ui/screen/mainmenu/uimainmenu.h
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uimenu.h"
-
-typedef struct cutscene_s cutscene_t;
-
-#define UI_MAIN_MENU_ITEM_COUNT 3
-#define UI_MAIN_MENU_WIDTH 200.0f
-#define UI_MAIN_MENU_HEIGHT 160.0f
-#define UI_MAIN_MENU_LABEL_MAX 32
-
-typedef struct {
- uimenu_t menu;
- uimenuitem_t items[UI_MAIN_MENU_ITEM_COUNT];
- char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX];
- char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX];
- char_t quitLabel[UI_MAIN_MENU_LABEL_MAX];
-} uimainmenu_t;
-
-extern uimainmenu_t UI_MAIN_MENU;
-
-/**
- * Initializes the main menu panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiMainMenuInit(void);
-
-/**
- * Opens the main menu panel. No-op when already open.
- */
-void uiMainMenuOpen(void);
-
-/**
- * Loads (or re-loads) the main menu cutscene asset (assets/cutscenes/
- * main_menu.jsonc, parsed directly at load time rather than compiled in)
- * and returns it - the shared accessor for the "Start Game"/Options/Quit
- * flows below to jump/restart it. Only one JSON-authored cutscene is ever
- * resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment in
- * cutscenesystem.h), so this resolves fresh on every call rather than
- * caching anything.
- *
- * @return The main menu cutscene, or NULL on failure (already reported via
- * uiFatalErrorOpen).
- */
-cutscene_t *uiMainMenuLoadCutscene(void);
-
-/**
- * Starts the "Start Game" flow: jumps the already-running main menu
- * cutscene to its NEW_GAME marker (or restarts it there if it already ran
- * to completion, e.g. the player backed out of the save picker back to
- * this menu) to load every save slot - retrying on error, prompting if no
- * save device is found - then arms uiMainMenuOpenSelectSave to open the
- * load-game picker once that succeeds. Called when the player selects
- * Start Game.
- */
-void uiMainMenuStartGame(void);
-
-/**
- * Cutscene completion callback armed by uiMainMenuStartGame: opens the
- * load-game picker once the NEW_GAME flow has finished loading every save
- * slot.
- *
- * @param userData Unused - required by cutscenecallback_t's signature.
- */
-void uiMainMenuOpenSelectSave(void *userData);
-
-/**
- * Result callback for the load-game picker opened by
- * uiMainMenuOpenSelectSave: re-opens this panel if backed out of without
- * choosing a slot, otherwise starts the game with the chosen slot.
- *
- * @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE.
- * @param user Unused - required by uiselectsaveresultcallback_t's signature.
- */
-void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user);
-
-/**
- * Draws the main menu panel, centered on screen. No-op when not active.
- *
- * @return Any error that occurs.
- */
-errorret_t uiMainMenuDraw(void);
-
-/**
- * Disposes of the main menu panel.
- *
- * @return Any error that occurs.
- */
-errorret_t uiMainMenuDispose(void);
diff --git a/src/dusk/ui/transition/CMakeLists.txt b/src/dusk/ui/transition/CMakeLists.txt
deleted file mode 100644
index 0e365038..00000000
--- a/src/dusk/ui/transition/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uitransition.c
- uitransitionfade.c
-)
diff --git a/src/dusk/ui/transition/uitransition.c b/src/dusk/ui/transition/uitransition.c
deleted file mode 100644
index 32f5dce9..00000000
--- a/src/dusk/ui/transition/uitransition.c
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitransition.h"
-#include "uitransitionfade.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "time/time.h"
-#include "util/math.h"
-
-uitransition_t UI_TRANSITION;
-
-uitransitiondrawfunc_t UI_TRANSITION_TYPE_DRAW[UI_TRANSITION_TYPE_COUNT] = {
- [UI_TRANSITION_TYPE_FADE] = uiTransitionFadeDraw
-};
-
-errorret_t uiTransitionInit(void) {
- memoryZero(&UI_TRANSITION, sizeof(uitransition_t));
-
- // uiTransitionStart((uitransitionstartparams_t){
- // .duration = 5.0f,
- // .type = UI_TRANSITION_TYPE_FADE,
- // .params = {
- // .fade = {
- // .easing = EASING_OUT_QUART,
- // .fromColor = COLOR_TRANSPARENT_BLACK,
- // .toColor = COLOR_BLACK
- // }
- // },
- // .finished = NULL,
- // .user = NULL
- // });
-
- errorOk();
-}
-
-errorret_t uiTransitionUpdate(void) {
- // Update time.
- float_t newTime = UI_TRANSITION.data.time + TIME.delta;
-
- UI_TRANSITION.data.time = mathClamp(
- newTime,
- 0.0f,
- UI_TRANSITION.data.duration
- );
- UI_TRANSITION.data.t = (
- (UI_TRANSITION.data.duration <= 0.0f) ?
- 1.0f :
- (UI_TRANSITION.data.time / UI_TRANSITION.data.duration)
- );
-
- if(UI_TRANSITION.data.t >= 1.0f && UI_TRANSITION.finished) {
- // Saved and cleared before firing so a callback that immediately
- // starts another transition isn't clobbered by this function's own
- // cleanup running after it - same reentrancy hazard as uiFocusPop,
- // see src/dusk/ui/focus/uifocus.c. Without this, finished would also
- // just keep re-firing every frame forever once t reaches 1.0.
- uitransitioncallback_t finished = UI_TRANSITION.finished;
- void *user = UI_TRANSITION.user;
- UI_TRANSITION.finished = NULL;
- finished(user);
- }
-
- errorOk();
-}
-
-errorret_t uiTransitionDraw(void) {
- uitransitiondrawfunc_t draw = UI_TRANSITION_TYPE_DRAW[UI_TRANSITION.type];
- if(!draw) errorOk();
- return draw(&UI_TRANSITION.data);
-}
-
-void uiTransitionStart(const uitransitionstartparams_t info) {
- assertTrue(info.type > UI_TRANSITION_TYPE_NULL, "Invalid type");
- assertTrue(info.type < UI_TRANSITION_TYPE_COUNT, "Invalid type");
- assertTrue(info.duration >= 0.0f, "Duration must be non-negative");
-
- memoryZero(&UI_TRANSITION, sizeof(uitransition_t));
-
- UI_TRANSITION.data.duration = info.duration;
- UI_TRANSITION.type = info.type;
- UI_TRANSITION.data.params = info.params;
- UI_TRANSITION.finished = info.finished;
- UI_TRANSITION.user = info.user;
-}
\ No newline at end of file
diff --git a/src/dusk/ui/transition/uitransition.h b/src/dusk/ui/transition/uitransition.h
deleted file mode 100644
index 305ec5c3..00000000
--- a/src/dusk/ui/transition/uitransition.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/color.h"
-#include "animation/easing.h"
-#include "uitransitiondata.h"
-
-typedef void (*uitransitioncallback_t)(void *user);
-
-typedef enum {
- UI_TRANSITION_TYPE_NULL,
-
- UI_TRANSITION_TYPE_FADE,
-
- UI_TRANSITION_TYPE_COUNT
-} uitransitiontype_t;
-
-extern uitransitiondrawfunc_t UI_TRANSITION_TYPE_DRAW[UI_TRANSITION_TYPE_COUNT];
-
-
-typedef struct {
- uitransitiondata_t data;
- uitransitiontype_t type;
- uitransitioncallback_t finished;
- void *user;
-} uitransition_t;
-
-extern uitransition_t UI_TRANSITION;
-
-/**
- * Initializes the transition overlay, zeroing all fields and wiring the
- * onTransitionEnd event.
- *
- * @return Any error that occurs.
- */
-errorret_t uiTransitionInit(void);
-
-/**
- * Advances the transition. Fires onTransitionEnd once when it completes.
- * Safe to call when no transition is running.
- *
- * @return Any error that occurs.
- */
-errorret_t uiTransitionUpdate(void);
-
-/**
- * Renders the transition overlay. Skipped entirely when the current alpha
- * is zero.
- *
- * @return Any error that occurs.
- */
-errorret_t uiTransitionDraw(void);
-
-typedef struct {
- float_t duration;
- uitransitiontype_t type;
- uitransitionparams_t params;
- uitransitioncallback_t finished;
- void *user;
-} uitransitionstartparams_t;
-
-/**
- * Starts a transition described by an info struct.
- *
- * @param info Transition parameters.
- */
-void uiTransitionStart(const uitransitionstartparams_t info);
-
diff --git a/src/dusk/ui/transition/uitransitiondata.h b/src/dusk/ui/transition/uitransitiondata.h
deleted file mode 100644
index 37e8430f..00000000
--- a/src/dusk/ui/transition/uitransitiondata.h
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "uitransitionfade.h"
-
-typedef union uitransitionparams_u {
- uitransitionfade_t fade;
-} uitransitionparams_t;
-
-typedef struct uitransitiondata_s {
- float_t time;
- float_t duration;
- float_t t;
- uitransitionparams_t params;
-} uitransitiondata_t;
-
-typedef errorret_t (*uitransitiondrawfunc_t)(const uitransitiondata_t *data);
diff --git a/src/dusk/ui/transition/uitransitionfade.c b/src/dusk/ui/transition/uitransitionfade.c
deleted file mode 100644
index 45e3168d..00000000
--- a/src/dusk/ui/transition/uitransitionfade.c
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitransition.h"
-#include "assert/assert.h"
-#include "display/screen/screen.h"
-#include "display/texture/texture.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-
-errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) {
- assertNotNull(data, "data must not be NULL");
-
- float_t e = easingApply(data->params.fade.easing, data->t);
-
- float_t r = (float_t)(
- data->params.fade.toColor.r - data->params.fade.fromColor.r
- ) * e;
- float_t g = (float_t)(
- data->params.fade.toColor.g - data->params.fade.fromColor.g
- ) * e;
- float_t b = (float_t)(
- data->params.fade.toColor.b - data->params.fade.fromColor.b
- ) * e;
- float_t a = (float_t)(
- data->params.fade.toColor.a - data->params.fade.fromColor.a
- ) * e;
-
- color_t color = color4b(
- data->params.fade.fromColor.r + (uint8_t)r,
- data->params.fade.fromColor.g + (uint8_t)g,
- data->params.fade.fromColor.b + (uint8_t)b,
- data->params.fade.fromColor.a + (uint8_t)a
- );
-
- if(color.a == 0) errorOk();
-
- spritebatchsprite_t sprite = {
- .min = { 0.0f, 0.0f, 0.0f },
- .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t material = {
- .unlit = {
- .color = color,
- .texture = NULL
- }
- };
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- return spriteBatchFlush();
-}
diff --git a/src/dusk/ui/transition/uitransitionfade.h b/src/dusk/ui/transition/uitransitionfade.h
deleted file mode 100644
index a1b2475a..00000000
--- a/src/dusk/ui/transition/uitransitionfade.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/color.h"
-#include "animation/easing.h"
-
-typedef struct uitransitiondata_s uitransitiondata_t;
-
-typedef struct {
- color_t fromColor;
- color_t toColor;
- easingtype_t easing;
-} uitransitionfade_t;
-
-/**
- * Renders a full-screen color overlay interpolated between fade->fromColor
- * and fade->toColor using fade->easing. t is the raw normalized progress
- * in [0, 1] before easing is applied. No-op when the resulting alpha is zero.
- *
- * @param fade The fade parameters.
- * @return Any error that occurs.
- */
-errorret_t uiTransitionFadeDraw(const uitransitiondata_t *fade);
diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c
deleted file mode 100644
index 2cd586d1..00000000
--- a/src/dusk/ui/ui.c
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "ui.h"
-#include "util/memory.h"
-#include "assert/assert.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/screen/screen.h"
-#include "ui/uielement.h"
-#include "ui/uielementlist.h"
-#include "ui/focus/uifocus.h"
-#include "log/log.h"
-
-ui_t UI;
-
-errorret_t uiInit(void) {
- memoryZero(&UI, sizeof(ui_t));
- uiFocusInit();
-
- uielement_t *element = &UI_ELEMENTS[0];
- while(!uiElementIsNull(element)) {
- errorChain(uiElementInit(element));
- element++;
- }
-
- errorOk();
-}
-
-errorret_t uiUpdate(void) {
- uiFocusUpdate();
-
- uielement_t *element = &UI_ELEMENTS[0];
- while(!uiElementIsNull(element)) {
- errorChain(uiElementUpdate(element));
- element++;
- }
-
- errorOk();
-}
-
-errorret_t uiRender(void) {
- const uielement_t *element = &UI_ELEMENTS[0];
- while(!uiElementIsNull(element)) {
- errorChain(uiElementDraw(element));
-
- if(SPRITEBATCH.spriteCount > 0) {
- logDebug("Finished UI element but unflushed sprites remain.\n");
- }
-
- element++;
- }
-
- errorOk();
-}
-
-errorret_t uiDispose(void) {
- uielement_t *element = &UI_ELEMENTS[0];
- while(!uiElementIsNull(element)) {
- errorChain(uiElementDispose(element));
- element++;
- }
- errorOk();
-}
-
-errorret_t uiUpdateTranslations(void) {
- uielement_t *element = &UI_ELEMENTS[0];
- while(!uiElementIsNull(element)) {
- errorChain(uiElementUpdateTranslations(element));
- element++;
- }
- errorOk();
-}
\ No newline at end of file
diff --git a/src/dusk/ui/ui.h b/src/dusk/ui/ui.h
deleted file mode 100644
index 8d6a88ad..00000000
--- a/src/dusk/ui/ui.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct {
- void *nothing;
-} ui_t;
-
-extern ui_t UI;
-
-/**
- * Initializes the UI system.
- */
-errorret_t uiInit(void);
-
-/**
- * Updates the UI system.
- *
- * @return Any error that occurs.
- */
-errorret_t uiUpdate(void);
-
-/**
- * Renders the UI system.
- *
- * @return Any error that occurs.
- */
-errorret_t uiRender(void);
-
-/**
- * Disposes of the UI system.
- *
- * @return Any error that occurs.
- */
-errorret_t uiDispose(void);
-
-/**
- * Notifies the UI system that the active locale has changed, so every
- * registered element can re-fetch any localized strings it caches.
- *
- * @return Any error that occurs.
- */
-errorret_t uiUpdateTranslations(void);
\ No newline at end of file
diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c
deleted file mode 100644
index af4c8cf3..00000000
--- a/src/dusk/ui/uielement.c
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uielement.h"
-#include "assert/assert.h"
-#include "ui/widget/uiframe.h"
-#include "engine/engine.h"
-
-bool_t uiElementIsNull(const uielement_t *element) {
- return element->init == NULL &&
- element->update == NULL &&
- element->draw == NULL &&
- element->dispose == NULL &&
- element->updateTranslations == NULL;
-}
-
-errorret_t uiElementInit(uielement_t *element) {
- assertNotNull(element, "element must not be NULL");
- errorChain(uiFrameInit());
- if(element->init != NULL) errorChain(element->init());
- errorOk();
-}
-
-errorret_t uiElementUpdate(uielement_t *element) {
- assertNotNull(element, "element must not be NULL");
- if(element->update != NULL) errorChain(element->update());
- errorOk();
-}
-
-errorret_t uiElementDraw(const uielement_t *element) {
- assertNotNull(element, "element must not be NULL");
- if(element->draw != NULL) errorChain(element->draw());
- errorOk();
-}
-
-errorret_t uiElementDispose(uielement_t *element) {
- assertNotNull(element, "element must not be NULL");
- if(element->dispose != NULL) errorChain(element->dispose());
- errorChain(uiFrameDispose());
- errorOk();
-}
-
-errorret_t uiElementUpdateTranslations(uielement_t *element) {
- assertNotNull(element, "element must not be NULL");
- if(element->updateTranslations != NULL) {
- errorChain(element->updateTranslations());
- }
- errorOk();
-}
\ No newline at end of file
diff --git a/src/dusk/ui/uielement.h b/src/dusk/ui/uielement.h
deleted file mode 100644
index d4fdc74f..00000000
--- a/src/dusk/ui/uielement.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct {
- errorret_t (*init)();
- errorret_t (*update)();
- errorret_t (*draw)();
- errorret_t (*dispose)();
- errorret_t (*updateTranslations)();
-} uielement_t;
-
-/**
- * Returns true when all callbacks on the element are NULL, which marks
- * the end of the UI_ELEMENTS array.
- *
- * @param element The element to test.
- * @returns True if the element is the null terminator.
- */
-bool_t uiElementIsNull(const uielement_t *element);
-
-/**
- * Initializes a UI element, invoking its init callback if set.
- *
- * @param element The element to initialize.
- * @return Any error that occurs.
- */
-errorret_t uiElementInit(uielement_t *element);
-
-/**
- * Updates a UI element, calling its update callback if set.
- *
- * @param element The element to update.
- * @return Any error that occurs.
- */
-errorret_t uiElementUpdate(uielement_t *element);
-
-/**
- * Draws a UI element, calling its draw callback if set.
- *
- * @param element The element to render.
- * @return Any error that occurs.
- */
-errorret_t uiElementDraw(const uielement_t *element);
-
-/**
- * Disposes of a UI element, invoking its dispose callback if set.
- *
- * @param element The element to dispose.
- * @return Any error that occurs.
- */
-errorret_t uiElementDispose(uielement_t *element);
-
-/**
- * Notifies a UI element that the active locale has changed, calling its
- * updateTranslations callback if set, so it can re-fetch any localized
- * strings it caches.
- *
- * @param element The element to notify.
- * @return Any error that occurs.
- */
-errorret_t uiElementUpdateTranslations(uielement_t *element);
diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c
deleted file mode 100644
index 9f3783dd..00000000
--- a/src/dusk/ui/uielementlist.c
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uielementlist.h"
-#include "ui/widget/uiframe.h"
-#include "ui/debug/uifps.h"
-#include "ui/overlay/uifullbox.h"
-#include "ui/overlay/uiloading.h"
-#include "ui/overlay/uifatalerror.h"
-#include "ui/debug/uiplayerpos.h"
-#include "ui/debug/uicutscene.h"
-#include "ui/overlay/uicrop.h"
-#include "ui/transition/uitransition.h"
-#include "ui/debug/uiconsole.h"
-#include "ui/screen/game/uigamemenu.h"
-#include "ui/screen/mainmenu/uimainmenu.h"
-#include "ui/screen/battle/uibattlemenu.h"
-#include "ui/screen/battle/uibattlehud.h"
-#include "ui/screen/backpack/uibackpack.h"
-#include "ui/dialog/uiconfirm.h"
-#include "ui/widget/uimodal.h"
-#include "ui/dialog/keyboard/uikeyboard.h"
-#include "ui/dialog/save/uiselectsave.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "ui/rpg/textbox/uitextboxminilist.h"
-#include "ui/rpg/uiemoji.h"
-
-uielement_t UI_ELEMENTS[] = {
- // ===========================================================================
- // Non-Rendered Components, just needs init and deinit
- // ===========================================================================
- {
- .init = uiFrameInit,
- .dispose = uiFrameDispose
- },
-
- // ===========================================================================
- // In-World Gameplay Components, sits below all UI but above gameplay.
- // ===========================================================================
- {
- .init = uiEmojiInit,
- .update = uiEmojiUpdate,
- .draw = uiEmojiDraw,
- .dispose = uiEmojiDispose
- },
-
- // ===========================================================================
- // Low rendered components, usually gameplay non-world components.
- // ===========================================================================
- {
- .init = uiFullboxUnderInit,
- .update = uiFullboxUnderUpdate,
- .draw = uiFullboxUnderDraw
- },
-
- // ===========================================================================
- // Most menus and non-gameplay components. Hierarchy is very important.
- // ===========================================================================
- {
- .init = uiGameMenuInit,
- .draw = uiGameMenuDraw,
- .dispose = uiGameMenuDispose
- },
- {
- .init = uiMainMenuInit,
- .draw = uiMainMenuDraw,
- .dispose = uiMainMenuDispose
- },
- {
- .init = uiBattleHudInit,
- .draw = uiBattleHudDraw
- },
- {
- .init = uiBattleMenuInit,
- .update = uiBattleMenuUpdate,
- .draw = uiBattleMenuDraw
- },
- {
- .init = uiBackpackInit,
- .draw = uiBackpackDraw,
- .dispose = uiBackpackDispose
- },
-
- // ===========================================================================
- // Modals, Popups and Dialogs, above most other UI things.
- // ===========================================================================
- {
- .init = uiSelectSaveInit,
- .draw = uiSelectSaveDraw,
- .dispose = uiSelectSaveDispose
- },
- {
- .init = uiTextboxMainInit,
- .update = uiTextboxMainUpdate,
- .draw = uiTextboxMainDraw
- },
- {
- .init = uiTextboxMiniListInit,
- .update = uiTextboxMiniListUpdate,
- .draw = uiTextboxMiniListDraw
- },
- {
- .init = uiKeyboardInit,
- .draw = uiKeyboardDraw,
- .dispose = uiKeyboardDispose
- },
- {
- .init = uiModalInit,
- .draw = uiModalDraw,
- .dispose = uiModalDispose
- },
- {
- .init = uiConfirmInit,
- .dispose = uiConfirmDispose
- },
-
- // ===========================================================================
- // Overlayed components, can even outdraw all UI things.
- // ===========================================================================
- {
- .init = uiTransitionInit,
- .update = uiTransitionUpdate,
- .draw = uiTransitionDraw
- },
- {
- .init = uiFullboxOverInit,
- .update = uiFullboxOverUpdate,
- .draw = uiFullboxOverDraw
- },
-
- {
- .init = uiLoadingInit,
- .update = uiLoadingUpdate,
- .draw = uiLoadingDraw
- },
- {
- .init = uiFatalErrorInit,
- .draw = uiFatalErrorDraw,
- .dispose = uiFatalErrorDispose
- },
-
- // ===========================================================================
- // Important rendering components, lives outside the primary game engine
- // ===========================================================================
- {
- .init = uiCropInit,
- .draw = uiCropDraw
- },
-
- // ===========================================================================
- // Debug components, disregards everything else.
- // ===========================================================================
- { .init = uiConsoleInit, .draw = uiConsoleDraw },
- { .init = uiFPSInit, .draw = uiFPSDraw },
- { .init = uiPlayerPosInit, .draw = uiPlayerPosDraw },
- { .init = uiCutsceneInit, .draw = uiCutsceneDraw },
-
- // ===========================================================================
- // Null terminator
- // ===========================================================================
- { 0 }
-};
diff --git a/src/dusk/ui/uielementlist.h b/src/dusk/ui/uielementlist.h
deleted file mode 100644
index 11bbca33..00000000
--- a/src/dusk/ui/uielementlist.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "ui/uielement.h"
-
-extern uielement_t UI_ELEMENTS[];
diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt
deleted file mode 100644
index e15d658a..00000000
--- a/src/dusk/ui/widget/CMakeLists.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-target_sources(${DUSK_LIBRARY_TARGET_NAME}
- PUBLIC
- uibutton.c
- uicheckbox.c
- uiframe.c
- uilabel.c
- uitab.c
- uislider.c
- uidropdown.c
- uiscrolling.c
- uiitem.c
- uiitemlist.c
- uimenu.c
- uimodal.c
-)
diff --git a/src/dusk/ui/widget/uibutton.c b/src/dusk/ui/widget/uibutton.c
deleted file mode 100644
index 789676af..00000000
--- a/src/dusk/ui/widget/uibutton.c
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uibutton.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "display/color.h"
-
-void uiButtonInit(uibutton_t *button, const char_t *label) {
- assertNotNull(button, "Button cannot be NULL");
- assertNotNull(label, "Label cannot be NULL");
-
- memoryZero(button, sizeof(uibutton_t));
- uiLabelInit(&button->label, label, button->sprites, UI_BUTTON_SPRITES_MAX);
- button->label.dirty = true;
-}
-
-bool_t uiButtonIsHighlighted(const uibutton_t *button) {
- assertNotNull(button, "Button cannot be NULL");
- return button->highlighted;
-}
-
-void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
- assertNotNull(button, "Button cannot be NULL");
- button->highlighted = highlighted;
-}
-
-bool_t uiButtonIsActive(const uibutton_t *button) {
- assertNotNull(button, "Button cannot be NULL");
- return button->active;
-}
-
-void uiButtonSetActive(uibutton_t *button, const bool_t active) {
- assertNotNull(button, "Button cannot be NULL");
- button->active = active;
-}
-
-errorret_t uiButtonDraw(
- uibutton_t *button,
- const float_t x,
- const float_t y
-) {
- assertNotNull(button, "Button cannot be NULL");
-
- uiLabelSetX(&button->label, x);
- uiLabelSetY(&button->label, y);
-
- return uiLabelRender(
- &button->label,
- button->active ? COLOR_GREEN :
- button->highlighted ? COLOR_RED :
- COLOR_WHITE
- );
-}
diff --git a/src/dusk/ui/widget/uibutton.h b/src/dusk/ui/widget/uibutton.h
deleted file mode 100644
index 87cd1a21..00000000
--- a/src/dusk/ui/widget/uibutton.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-
-#define UI_BUTTON_SPRITES_MAX 32
-
-typedef struct {
- uilabel_t label;
- spritebatchsprite_t sprites[UI_BUTTON_SPRITES_MAX];
- bool_t highlighted;
- // Independent of highlighted (which the focus system drives based on
- // what's currently focused) - for a button that also needs to show a
- // toggled-on state regardless of focus, e.g. the keyboard's CAPS/SHIFT
- // keys.
- bool_t active;
-} uibutton_t;
-
-/**
- * Initializes a button.
- *
- * @param button The button to initialize.
- * @param label Display label; never written to, safe to be a caller-
- * owned or immutable string.
- */
-void uiButtonInit(uibutton_t *button, const char_t *label);
-
-/**
- * Returns whether the button is highlighted.
- *
- * @param button The button to query.
- * @returns True if highlighted.
- */
-bool_t uiButtonIsHighlighted(const uibutton_t *button);
-
-/**
- * Sets the highlighted state of the button.
- *
- * @param button The button to update.
- * @param highlighted The new highlighted state.
- */
-void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted);
-
-/**
- * Returns whether the button is active.
- *
- * @param button The button to query.
- * @returns True if active.
- */
-bool_t uiButtonIsActive(const uibutton_t *button);
-
-/**
- * Sets the active state of the button - a toggled-on indicator drawn
- * the same way as highlighted, but independent of focus.
- *
- * @param button The button to update.
- * @param active The new active state.
- */
-void uiButtonSetActive(uibutton_t *button, const bool_t active);
-
-/**
- * Draws the button at the given screen position.
- *
- * @param button The button to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiButtonDraw(
- uibutton_t *button,
- const float_t x,
- const float_t y
-);
diff --git a/src/dusk/ui/widget/uicheckbox.c b/src/dusk/ui/widget/uicheckbox.c
deleted file mode 100644
index f9cc8a55..00000000
--- a/src/dusk/ui/widget/uicheckbox.c
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uicheckbox.h"
-#include "util/memory.h"
-#include "display/text/text.h"
-#include "display/color.h"
-
-void uiCheckboxInit(
- uicheckbox_t *checkbox,
- const char_t *label
-) {
- memoryZero(checkbox, sizeof(uicheckbox_t));
- checkbox->label = label;
-}
-
-bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
- return checkbox->checked;
-}
-
-void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) {
- checkbox->checked = checked;
-}
-
-void uiCheckboxToggle(uicheckbox_t *checkbox) {
- uiCheckboxSetChecked(checkbox, !uiCheckboxIsChecked(checkbox));
-}
-
-bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) {
- return checkbox->highlighted;
-}
-
-void uiCheckboxSetHighlighted(
- uicheckbox_t *checkbox, const bool_t highlighted
-) {
- checkbox->highlighted = highlighted;
-}
-
-errorret_t uiCheckboxDraw(
- const uicheckbox_t *checkbox,
- const float_t x,
- const float_t y
-) {
- color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE;
- const char_t *mark = checkbox->checked ? "Y " : "N ";
- errorChain(textDraw(x, y, mark, color, &FONT_DEFAULT));
-
- int32_t markW, markH;
- textMeasure(mark, &FONT_DEFAULT, &markW, &markH);
- errorChain(textDraw(
- x + (float_t)markW, y, checkbox->label, color, &FONT_DEFAULT
- ));
-
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uicheckbox.h b/src/dusk/ui/widget/uicheckbox.h
deleted file mode 100644
index b32d52cc..00000000
--- a/src/dusk/ui/widget/uicheckbox.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct uicheckbox_s {
- const char_t *label;
- bool_t checked;
- bool_t highlighted;
-} uicheckbox_t;
-
-/**
- * Initializes a checkbox.
- *
- * @param checkbox The checkbox to initialize.
- * @param label Display label.
- */
-void uiCheckboxInit(
- uicheckbox_t *checkbox,
- const char_t *label
-);
-
-/**
- * Returns whether the checkbox is checked.
- *
- * @param checkbox The checkbox to query.
- * @returns True if checked.
- */
-bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
-
-/**
- * Sets the checked state of the checkbox.
- *
- * @param checkbox The checkbox to update.
- * @param checked The new checked state.
- */
-void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked);
-
-/**
- * Toggles the checked state of the checkbox.
- *
- * @param checkbox The checkbox to toggle.
- */
-void uiCheckboxToggle(uicheckbox_t *checkbox);
-
-/**
- * Returns whether the checkbox is highlighted.
- *
- * @param checkbox The checkbox to query.
- * @returns True if highlighted.
- */
-bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
-
-/**
- * Sets the highlighted state of the checkbox.
- *
- * @param checkbox The checkbox to update.
- * @param highlighted The new highlighted state.
- */
-void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted);
-
-/**
- * Draws the checkbox at the given screen position.
- *
- * @param checkbox The checkbox to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiCheckboxDraw(
- const uicheckbox_t *checkbox,
- const float_t x,
- const float_t y
-);
diff --git a/src/dusk/ui/widget/uidropdown.c b/src/dusk/ui/widget/uidropdown.c
deleted file mode 100644
index ae72bd4e..00000000
--- a/src/dusk/ui/widget/uidropdown.c
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uidropdown.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/text/text.h"
-#include "display/color.h"
-
-void uiDropdownInit(
- uidropdown_t *dropdown,
- const char_t *label,
- const char_t *const *options,
- const uint8_t optionCount,
- const uint8_t selectedIndex
-) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- assertNotNull(label, "Label cannot be NULL");
- assertNotNull(options, "Options cannot be NULL");
- assertTrue(optionCount > 0, "Dropdown must have at least one option");
-
- memoryZero(dropdown, sizeof(uidropdown_t));
- dropdown->label = label;
- dropdown->options = options;
- dropdown->optionCount = optionCount;
- dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0;
-}
-
-uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- return dropdown->selectedIndex;
-}
-
-const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- return dropdown->options[dropdown->selectedIndex];
-}
-
-void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- assertTrue(index < dropdown->optionCount, "Dropdown index out of range");
- dropdown->selectedIndex = index;
-}
-
-void uiDropdownStepNext(uidropdown_t *dropdown) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- dropdown->selectedIndex =
- (dropdown->selectedIndex + 1) % dropdown->optionCount;
-}
-
-void uiDropdownStepPrev(uidropdown_t *dropdown) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- dropdown->selectedIndex = dropdown->selectedIndex == 0 ?
- dropdown->optionCount - 1 : dropdown->selectedIndex - 1;
-}
-
-bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- return dropdown->highlighted;
-}
-
-void uiDropdownSetHighlighted(
- uidropdown_t *dropdown,
- const bool_t highlighted
-) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
- dropdown->highlighted = highlighted;
-}
-
-errorret_t uiDropdownDraw(
- const uidropdown_t *dropdown,
- const float_t x,
- const float_t y
-) {
- assertNotNull(dropdown, "Dropdown cannot be NULL");
-
- color_t color = dropdown->highlighted ? COLOR_RED : COLOR_WHITE;
-
- errorChain(textDraw(x, y, dropdown->label, color, &FONT_DEFAULT));
-
- int32_t labelW, labelH;
- textMeasure(dropdown->label, &FONT_DEFAULT, &labelW, &labelH);
-
- char_t valueText[UI_DROPDOWN_VALUE_TEXT_MAX];
- stringFormat(
- valueText, UI_DROPDOWN_VALUE_TEXT_MAX - 1, "< %s >",
- uiDropdownGetSelectedOption(dropdown)
- );
-
- errorChain(textDraw(
- x + (float_t)labelW + UI_DROPDOWN_GAP, y, valueText, color, &FONT_DEFAULT
- ));
-
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uidropdown.h b/src/dusk/ui/widget/uidropdown.h
deleted file mode 100644
index 0c6884ac..00000000
--- a/src/dusk/ui/widget/uidropdown.h
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_DROPDOWN_GAP 4.0f
-#define UI_DROPDOWN_VALUE_TEXT_MAX 64
-
-typedef struct {
- const char_t *label;
- const char_t *const *options;
- uint8_t optionCount;
- uint8_t selectedIndex;
- bool_t highlighted;
-} uidropdown_t;
-
-/**
- * Initializes a dropdown.
- *
- * @param dropdown The dropdown to initialize.
- * @param label Display label.
- * @param options Array of option display strings; caller-owned, must
- * outlive the dropdown.
- * @param optionCount Number of entries in options. Must be > 0.
- * @param selectedIndex Initial selected option index, clamped to
- * [0, optionCount - 1].
- */
-void uiDropdownInit(
- uidropdown_t *dropdown,
- const char_t *label,
- const char_t *const *options,
- const uint8_t optionCount,
- const uint8_t selectedIndex
-);
-
-/**
- * Returns the index of the currently selected option.
- *
- * @param dropdown The dropdown to query.
- * @returns The selected option index.
- */
-uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
-
-/**
- * Returns the currently selected option's display string.
- *
- * @param dropdown The dropdown to query.
- * @returns The selected option string.
- */
-const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
-
-/**
- * Sets the selected option by index.
- *
- * @param dropdown The dropdown to update.
- * @param index The new selected option index. Must be < optionCount.
- */
-void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index);
-
-/**
- * Selects the next option, wrapping around to the first option past
- * the last.
- *
- * @param dropdown The dropdown to update.
- */
-void uiDropdownStepNext(uidropdown_t *dropdown);
-
-/**
- * Selects the previous option, wrapping around to the last option
- * before the first.
- *
- * @param dropdown The dropdown to update.
- */
-void uiDropdownStepPrev(uidropdown_t *dropdown);
-
-/**
- * Returns whether the dropdown is highlighted.
- *
- * @param dropdown The dropdown to query.
- * @returns True if highlighted.
- */
-bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
-
-/**
- * Sets the highlighted state of the dropdown.
- *
- * @param dropdown The dropdown to update.
- * @param highlighted The new highlighted state.
- */
-void uiDropdownSetHighlighted(
- uidropdown_t *dropdown,
- const bool_t highlighted
-);
-
-/**
- * Draws the dropdown at the given screen position: label, then the
- * currently selected option surrounded by cycle arrows.
- *
- * @param dropdown The dropdown to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiDropdownDraw(
- const uidropdown_t *dropdown,
- const float_t x,
- const float_t y
-);
diff --git a/src/dusk/ui/widget/uiframe.c b/src/dusk/ui/widget/uiframe.c
deleted file mode 100644
index 5202b3e1..00000000
--- a/src/dusk/ui/widget/uiframe.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiframe.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "display/texture/texture.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-#include "display/color.h"
-
-uiframe_t UI_FRAME;
-
-errorret_t uiFrameInit(void) {
- memoryZero(&UI_FRAME, sizeof(uiframe_t));
-
- // Init texture.
- color_t border = color4b(0, 50, 255, 255);
- color_t center = color4b(0, 100, 220, 240);
-
- for(uint8_t y = 0; y < UI_FRAME_TEXTURE_HEIGHT_POW2; y++) {
- for(uint8_t x = 0; x < UI_FRAME_TEXTURE_WIDTH_POW2; x++) {
- color_t c;
- if(x >= UI_FRAME_TEXTURE_WIDTH || y >= UI_FRAME_TEXTURE_HEIGHT) {
- c = COLOR_TRANSPARENT;
- } else if(
- y < UI_FRAME_TILE_HEIGHT || y >= UI_FRAME_TILE_HEIGHT * 2 ||
- x < UI_FRAME_TILE_WIDTH || x >= UI_FRAME_TILE_WIDTH * 2
- ) {
- c = border;
- } else {
- c = center;
- }
- UI_FRAME.pixels[y * UI_FRAME_TEXTURE_WIDTH_POW2 + x] = c;
- }
- }
-
- errorChain(textureInit(
- &UI_FRAME.texture,
- UI_FRAME_TEXTURE_WIDTH_POW2, UI_FRAME_TEXTURE_HEIGHT_POW2,
- TEXTURE_FORMAT_RGBA,
- (texturedata_t){ .rgbaColors = UI_FRAME.pixels }
- ));
-
- UI_FRAME.tileset.tileWidth = UI_FRAME_TILE_WIDTH;
- UI_FRAME.tileset.tileHeight = UI_FRAME_TILE_HEIGHT;
- UI_FRAME.tileset.columns = 3;
- UI_FRAME.tileset.rows = 3;
- UI_FRAME.tileset.tileCount = 9;
- UI_FRAME.tileset.uv[0] =
- (float_t)UI_FRAME_TILE_WIDTH / UI_FRAME_TEXTURE_WIDTH_POW2;
- UI_FRAME.tileset.uv[1] =
- (float_t)UI_FRAME_TILE_HEIGHT / UI_FRAME_TEXTURE_HEIGHT_POW2;
-
- errorOk();
-}
-
-errorret_t uiFrameDraw(
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-) {
- shadermaterial_t material = {
- .unlit = {
- .color = COLOR_WHITE,
- .texture = &UI_FRAME.texture
- }
- };
- spritebatchsprite_t sprites[9];
- float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
- float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
-
- sprites[0] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 0, 0,
- x, y, tileW, tileH
- );
- sprites[1] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 1, 0,
- x + tileW, y, width - (tileW * 2.0f), tileH
- );
- sprites[2] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 2, 0,
- x + width - tileW, y, tileW, tileH
- );
- sprites[3] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 0, 1,
- x, y + tileH, tileW, height - (tileH * 2.0f)
- );
- sprites[4] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 1, 1,
- x + tileW, y + tileH,
- width - (tileW * 2.0f), height - (tileH * 2.0f)
- );
- sprites[5] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 2, 1,
- x + width - tileW, y + tileH, tileW, height - (tileH * 2.0f)
- );
- sprites[6] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 0, 2,
- x, y + height - tileH, tileW, tileH
- );
- sprites[7] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 1, 2,
- x + tileW, y + height - tileH, width - (tileW * 2.0f), tileH
- );
- sprites[8] = spriteBatchSpriteTilesetPosition(
- &UI_FRAME.tileset, 2, 2,
- x + width - tileW, y + height - tileH, tileW, tileH
- );
-
- return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
-}
-
-errorret_t uiFrameDispose(void) {
- return textureDispose(&UI_FRAME.texture);
-}
diff --git a/src/dusk/ui/widget/uiframe.h b/src/dusk/ui/widget/uiframe.h
deleted file mode 100644
index 626afd64..00000000
--- a/src/dusk/ui/widget/uiframe.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/texture/texture.h"
-#include "display/texture/tileset.h"
-
-#define UI_FRAME_BORDER_WIDTH 6
-#define UI_FRAME_BORDER_HEIGHT 6
-#define UI_FRAME_PADDING_X 2
-#define UI_FRAME_PADDING_Y 2
-#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
-#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
-#define UI_FRAME_TILE_WIDTH 1
-#define UI_FRAME_TILE_HEIGHT 1
-#define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3)
-#define UI_FRAME_TEXTURE_HEIGHT (UI_FRAME_TILE_HEIGHT * 3)
-#define UI_FRAME_TEXTURE_WIDTH_POW2 4
-#define UI_FRAME_TEXTURE_HEIGHT_POW2 4
-
-typedef struct {
- tileset_t tileset;
- texture_t texture;
- color_t pixels[UI_FRAME_BORDER_WIDTH * UI_FRAME_BORDER_HEIGHT];
-} uiframe_t;
-
-extern uiframe_t UI_FRAME;
-
-/**
- * Initializes the global UI_FRAME: builds the dummy texture and
- * configures the tileset. Call once after displayInit().
- *
- * @return Any error that occurs.
- */
-errorret_t uiFrameInit(void);
-
-/**
- * Draws UI_FRAME using 9-slice rendering from its 3x3 tileset.
- * Pushes quads to the sprite batch without flushing.
- *
- * @param x Screen x position.
- * @param y Screen y position.
- * @param width Total width of the frame.
- * @param height Total height of the frame.
- * @return Any error that occurs.
- */
-errorret_t uiFrameDraw(
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-);
-
-/**
- * Disposes of the global UI_FRAME, releasing its GPU texture.
- *
- * @return Any error that occurs.
- */
-errorret_t uiFrameDispose(void);
diff --git a/src/dusk/ui/widget/uiitem.c b/src/dusk/ui/widget/uiitem.c
deleted file mode 100644
index 9e73b676..00000000
--- a/src/dusk/ui/widget/uiitem.c
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiitem.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/text/text.h"
-#include "display/color.h"
-
-#define UI_ITEM_LABEL_MAX 48
-
-errorret_t uiItemInit(
- uiitem_t *item,
- const itemid_t itemId,
- const uint8_t quantity
-) {
- assertNotNull(item, "Item cannot be NULL");
- memoryZero(item, sizeof(uiitem_t));
- item->item = itemId;
- item->quantity = quantity;
-
- if(itemId == ITEM_ID_NULL) errorOk();
-
- errorChain(itemGetName(itemId, item->nameLabel, UI_ITEM_NAME_LABEL_MAX));
-
- errorOk();
-}
-
-itemid_t uiItemGetItem(const uiitem_t *item) {
- assertNotNull(item, "Item cannot be NULL");
- return item->item;
-}
-
-uint8_t uiItemGetQuantity(const uiitem_t *item) {
- assertNotNull(item, "Item cannot be NULL");
- return item->quantity;
-}
-
-void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity) {
- assertNotNull(item, "Item cannot be NULL");
- item->quantity = quantity;
-}
-
-bool_t uiItemIsHighlighted(const uiitem_t *item) {
- assertNotNull(item, "Item cannot be NULL");
- return item->highlighted;
-}
-
-void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted) {
- assertNotNull(item, "Item cannot be NULL");
- item->highlighted = highlighted;
-}
-
-errorret_t uiItemDraw(
- const uiitem_t *item,
- const float_t x,
- const float_t y
-) {
- assertNotNull(item, "Item cannot be NULL");
- if(item->item == ITEM_ID_NULL) errorOk();
-
- const color_t color = item->highlighted ? COLOR_RED : COLOR_WHITE;
-
- char_t text[UI_ITEM_LABEL_MAX];
- stringFormat(
- text, UI_ITEM_LABEL_MAX - 1,
- "%s x%u", item->nameLabel, item->quantity
- );
-
- errorChain(textDraw(x, y, text, color, &FONT_DEFAULT));
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uiitem.h b/src/dusk/ui/widget/uiitem.h
deleted file mode 100644
index 787884c8..00000000
--- a/src/dusk/ui/widget/uiitem.h
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "rpg/item/item.h"
-
-#define UI_ITEM_NAME_LABEL_MAX 32
-
-typedef struct {
- itemid_t item;
- uint8_t quantity;
- bool_t highlighted;
- char_t nameLabel[UI_ITEM_NAME_LABEL_MAX];
-} uiitem_t;
-
-/**
- * Initializes an item widget, resolving the item's localized name.
- *
- * @param item The item widget to initialize.
- * @param itemId The item ID to display. ITEM_ID_NULL renders as an
- * empty slot.
- * @param quantity The stack quantity to display.
- * @return Any error that occurs.
- */
-errorret_t uiItemInit(
- uiitem_t *item,
- const itemid_t itemId,
- const uint8_t quantity
-);
-
-/**
- * Returns the item ID this widget displays.
- *
- * @param item The item widget to query.
- * @returns The item ID.
- */
-itemid_t uiItemGetItem(const uiitem_t *item);
-
-/**
- * Returns the stack quantity this widget displays.
- *
- * @param item The item widget to query.
- * @returns The quantity.
- */
-uint8_t uiItemGetQuantity(const uiitem_t *item);
-
-/**
- * Sets the stack quantity this widget displays.
- *
- * @param item The item widget to update.
- * @param quantity The new quantity.
- */
-void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity);
-
-/**
- * Returns whether the item widget is highlighted.
- *
- * @param item The item widget to query.
- * @returns True if highlighted.
- */
-bool_t uiItemIsHighlighted(const uiitem_t *item);
-
-/**
- * Sets the highlighted state of the item widget.
- *
- * @param item The item widget to update.
- * @param highlighted The new highlighted state.
- */
-void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted);
-
-/**
- * Draws the item widget at the given screen position: item name and
- * quantity. No-op for an empty (ITEM_ID_NULL) slot.
- *
- * @param item The item widget to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiItemDraw(
- const uiitem_t *item,
- const float_t x,
- const float_t y
-);
diff --git a/src/dusk/ui/widget/uiitemlist.c b/src/dusk/ui/widget/uiitemlist.c
deleted file mode 100644
index 2ab2f62a..00000000
--- a/src/dusk/ui/widget/uiitemlist.c
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiitemlist.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "display/text/text.h"
-
-void uiItemListInit(
- uiitemlist_t *list,
- const uint8_t columns,
- const uint8_t rows,
- const uint8_t itemColumns,
- uiitemlistselectedcallback_t selected,
- uiitemlistchangedcallback_t changed,
- uiitemlistclosedcallback_t closed,
- uiitemlistcolumncallback_t columnDraw
-) {
- assertNotNull(list, "Item list cannot be NULL");
- assertTrue(columns > 0, "Item list columns must be > 0");
- assertTrue(rows > 0, "Item list rows must be > 0");
- assertTrue(itemColumns > 0, "Item list itemColumns must be > 0");
-
- memoryZero(list, sizeof(uiitemlist_t));
- list->columns = columns;
- list->rows = rows;
- list->itemColumns = itemColumns;
- list->selected = selected;
- list->changed = changed;
- list->closed = closed;
- list->columnDraw = columnDraw;
-
- uiScrollingInit(&list->scroll);
-}
-
-void uiItemListSetItems(
- uiitemlist_t *list,
- const uiitem_t *items,
- const uint8_t itemCount
-) {
- assertNotNull(list, "Item list cannot be NULL");
- assertTrue(
- itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
- );
-
- memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
- list->itemCount = itemCount;
-}
-
-errorret_t uiItemListSetItemStacks(
- uiitemlist_t *list,
- const inventorystack_t *stacks,
- const uint8_t stackCount
-) {
- assertNotNull(list, "Item list cannot be NULL");
- assertTrue(
- stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
- );
-
- for(uint8_t i = 0; i < stackCount; i++) {
- errorChain(
- uiItemInit(&list->items[i], stacks[i].item, stacks[i].quantity)
- );
- }
- list->itemCount = stackCount;
-
- errorOk();
-}
-
-void uiItemListOpen(uiitemlist_t *list) {
- assertNotNull(list, "Item list cannot be NULL");
- if(list->focusItem != NULL) return;
-
- list->focusItem = uiFocusPush(
- list->columns, list->rows,
- uiItemListFocusSelected,
- uiItemListFocusChanged,
- uiItemListFocusClosed,
- NULL,
- list
- );
-}
-
-void uiItemListClose(uiitemlist_t *list) {
- assertNotNull(list, "Item list cannot be NULL");
- if(list->focusItem == NULL) return;
- uiFocusPopItem(list->focusItem);
- list->focusItem = NULL;
-}
-
-bool_t uiItemListIsActive(const uiitemlist_t *list) {
- assertNotNull(list, "Item list cannot be NULL");
- return list->focusItem != NULL;
-}
-
-errorret_t uiItemListDraw(
- const uiitemlist_t *list,
- const float_t x,
- const float_t y,
- const float_t width
-) {
- assertNotNull(list, "Item list cannot be NULL");
- if(list->itemCount == 0) errorOk();
-
- const float_t colStep = width / (float_t)list->columns;
- const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- const float_t itemColStep = colStep / (float_t)list->itemColumns;
-
- const uint8_t visibleMax = list->columns * list->rows;
- const uint8_t drawCount =
- list->itemCount < visibleMax ? list->itemCount : visibleMax;
-
- for(uint8_t i = 0; i < drawCount; i++) {
- const uint8_t col = i % list->columns;
- const uint8_t row = i / list->columns;
-
- const float_t ix = x + (float_t)col * colStep;
- const float_t iy = y + (float_t)row * rowHeight;
-
- errorChain(uiItemDraw(&list->items[i], ix, iy));
-
- for(uint8_t c = 1; c < list->itemColumns; c++) {
- if(list->columnDraw == NULL) continue;
- errorChain(list->columnDraw(
- list, &list->items[i], c, ix + (float_t)c * itemColStep, iy
- ));
- }
- }
-
- errorOk();
-}
-
-bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
- if(list->selected == NULL) return true;
-
- const uint8_t index = focusItem->y * list->columns + focusItem->x;
- if(index >= list->itemCount) return true;
-
- list->selected(list, index, &list->items[index]);
- return true;
-}
-
-bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
-
- const uint8_t focusIndex = focusItem->y * list->columns + focusItem->x;
- for(uint8_t i = 0; i < list->itemCount; i++) {
- uiItemSetHighlighted(&list->items[i], i == focusIndex);
- }
-
- if(list->changed == NULL) return true;
- if(focusIndex >= list->itemCount) return true;
-
- list->changed(list, focusIndex, &list->items[focusIndex]);
- return true;
-}
-
-bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uiitemlist_t *list = (uiitemlist_t *)focusItem->user;
- list->focusItem = NULL;
-
- for(uint8_t i = 0; i < list->itemCount; i++) {
- uiItemSetHighlighted(&list->items[i], false);
- }
-
- if(list->closed != NULL) list->closed(list);
- return true;
-}
diff --git a/src/dusk/ui/widget/uiitemlist.h b/src/dusk/ui/widget/uiitemlist.h
deleted file mode 100644
index 26b3e350..00000000
--- a/src/dusk/ui/widget/uiitemlist.h
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uiitem.h"
-#include "ui/widget/uiscrolling.h"
-#include "ui/focus/uifocus.h"
-#include "rpg/item/inventory.h"
-
-#define UI_ITEM_LIST_CAPACITY_MAX 40
-
-typedef struct uiitemlist_s uiitemlist_t;
-
-typedef void (*uiitemlistselectedcallback_t)(
- const uiitemlist_t *list,
- const uint8_t index,
- const uiitem_t *item
-);
-
-typedef void (*uiitemlistchangedcallback_t)(
- const uiitemlist_t *list,
- const uint8_t index,
- const uiitem_t *item
-);
-
-typedef void (*uiitemlistclosedcallback_t)(const uiitemlist_t *list);
-
-/**
- * Called to draw an extra per-item info column (columnIndex >= 1)
- * beyond the item's own default uiItemDraw rendering -- e.g. a shop
- * menu drawing a price alongside the item name.
- *
- * @param list The item list being drawn.
- * @param item The item slot being drawn.
- * @param columnIndex The info column being drawn (>= 1).
- * @param x Screen x position for this column.
- * @param y Screen y position for this column.
- * @return Any error that occurs.
- */
-typedef errorret_t (*uiitemlistcolumncallback_t)(
- const uiitemlist_t *list,
- const uiitem_t *item,
- const uint8_t columnIndex,
- const float_t x,
- const float_t y
-);
-
-struct uiitemlist_s {
- uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX];
- uint8_t itemCount;
-
- // Grid layout: how many item slots wide/tall the list displays.
- uint8_t columns;
- uint8_t rows;
-
- // How many info columns are rendered per item slot. 1 means only the
- // item's own default name/quantity is drawn; anything beyond that is
- // rendered via columnDraw.
- uint8_t itemColumns;
-
- uiscrolling_t scroll;
- uifocusitem_t *focusItem;
-
- uiitemlistselectedcallback_t selected;
- uiitemlistchangedcallback_t changed;
- uiitemlistclosedcallback_t closed;
- uiitemlistcolumncallback_t columnDraw;
-
- void *user;
-};
-
-/**
- * Initializes an item list.
- *
- * @param list The item list to initialize.
- * @param columns Number of item slots per row.
- * @param rows Number of item slot rows.
- * @param itemColumns Number of info columns rendered per item slot.
- * @param selected Called when an item slot is selected.
- * @param changed Called when the focused item slot changes.
- * @param closed Called when the item list is closed.
- * @param columnDraw Called to draw each extra info column (index >= 1);
- * may be NULL if itemColumns is 1.
- */
-void uiItemListInit(
- uiitemlist_t *list,
- const uint8_t columns,
- const uint8_t rows,
- const uint8_t itemColumns,
- uiitemlistselectedcallback_t selected,
- uiitemlistchangedcallback_t changed,
- uiitemlistclosedcallback_t closed,
- uiitemlistcolumncallback_t columnDraw
-);
-
-/**
- * Sets the items displayed by the list directly, copying them into
- * the list's own storage.
- *
- * @param list The item list to update.
- * @param items The items to display.
- * @param itemCount Number of entries in items. Must be <=
- * UI_ITEM_LIST_CAPACITY_MAX.
- */
-void uiItemListSetItems(
- uiitemlist_t *list,
- const uiitem_t *items,
- const uint8_t itemCount
-);
-
-/**
- * Sets the items displayed by the list from an array of item stacks,
- * internally creating a uiitem_t for each stack.
- *
- * @param list The item list to update.
- * @param stacks The item stacks to display.
- * @param stackCount Number of entries in stacks. Must be <=
- * UI_ITEM_LIST_CAPACITY_MAX.
- * @return Any error that occurs.
- */
-errorret_t uiItemListSetItemStacks(
- uiitemlist_t *list,
- const inventorystack_t *stacks,
- const uint8_t stackCount
-);
-
-/**
- * Pushes the item list onto the UI focus stack, making it navigable.
- * No-op if already open.
- *
- * @param list The item list to open.
- */
-void uiItemListOpen(uiitemlist_t *list);
-
-/**
- * Pops the item list from the UI focus stack. No-op if already closed.
- *
- * @param list The item list to close.
- */
-void uiItemListClose(uiitemlist_t *list);
-
-/**
- * Returns whether the item list is currently on the UI focus stack.
- *
- * @param list The item list to query.
- * @returns True if active.
- */
-bool_t uiItemListIsActive(const uiitemlist_t *list);
-
-/**
- * Draws the item list's grid of item slots at the given position.
- * Only the first columns * rows items are drawn.
- *
- * @param list The item list to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @param width Content width, used to lay out columns.
- * @return Any error that occurs.
- */
-errorret_t uiItemListDraw(
- const uiitemlist_t *list,
- const float_t x,
- const float_t y,
- const float_t width
-);
-
-/**
- * Internal focus callback -- forwards selection to the list's selected
- * handler.
- *
- * @param focusItem The active focus item; user field must point to
- * uiitemlist_t.
- * @returns True.
- */
-bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem);
-
-/**
- * Internal focus callback -- updates item highlights and fires
- * changed.
- *
- * @param focusItem The active focus item; user field must point to
- * uiitemlist_t.
- * @returns True.
- */
-bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem);
-
-/**
- * Internal focus callback -- clears focusItem and fires the closed
- * handler.
- *
- * @param focusItem The active focus item; user field must point to
- * uiitemlist_t.
- * @returns True.
- */
-bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem);
diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c
deleted file mode 100644
index 48109474..00000000
--- a/src/dusk/ui/widget/uilabel.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uilabel.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "display/text/text.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/shader/shaderunlit.h"
-
-void uiLabelInit(
- uilabel_t *label,
- const char_t *text,
- spritebatchsprite_t *sprites,
- const int32_t spritesMax
-) {
- assertNotNull(text, "Text buffer cannot be NULL");
- assertNotNull(sprites, "Sprites buffer cannot be NULL");
-
- memoryZero(label, sizeof(uilabel_t));
- label->text = text;
- label->sprites = sprites;
- label->spritesMax = spritesMax;
- label->dirty = true;
-}
-
-void uiLabelSetWrap(
- uilabel_t *label,
- char_t *wrapBuffer,
- const uint32_t wrapBufferMax,
- const float_t wrapWidth
-) {
- assertNotNull(label, "Label cannot be NULL");
-
- // A NULL wrapBuffer always disables wrapping outright, regardless of
- // wrapWidth - so callers can unconditionally pass NULL to turn wrapping
- // off without also having to zero out whatever width they were using.
- // Enabling it (a non-NULL buffer) always requires a positive width.
- if(wrapBuffer != NULL) {
- assertTrue(wrapWidth > 0.0f, "Wrap width must be greater than 0");
- assertTrue(wrapBufferMax >= 1, "Wrap buffer max must be at least 1");
- }
-
- label->wrapBuffer = wrapBuffer;
- label->wrapBufferMax = wrapBufferMax;
- label->wrapWidth = wrapBuffer == NULL ? 0.0f : wrapWidth;
- label->dirty = true;
-}
-
-void uiLabelSetX(uilabel_t *label, const float_t x) {
- if(label->x == x) return;
- label->x = x;
- label->dirty = true;
-}
-
-float_t uiLabelGetX(const uilabel_t *label) {
- return label->x;
-}
-
-void uiLabelSetY(uilabel_t *label, const float_t y) {
- if(label->y == y) return;
- label->y = y;
- label->dirty = true;
-}
-
-float_t uiLabelGetY(const uilabel_t *label) {
- return label->y;
-}
-
-void uiLabelRebuffer(uilabel_t *label) {
- if(!label->dirty) return;
-
- const char_t *renderText = label->text;
- if(label->wrapWidth > 0.0f) {
- assertNotNull(
- label->wrapBuffer, "Wrap buffer cannot be NULL when wrapping is enabled"
- );
- stringCopy(label->wrapBuffer, label->text, label->wrapBufferMax);
- textWrap(label->wrapBuffer, &FONT_DEFAULT, label->wrapWidth);
- renderText = label->wrapBuffer;
- }
-
- textMeasure(renderText, &FONT_DEFAULT, &label->width, &label->height);
-
- int32_t charIndex = 0;
- float_t posX = label->x;
- float_t posY = label->y;
- label->spriteCount = textBuffer(
- label->x, label->y, renderText, &FONT_DEFAULT,
- label->sprites, label->spritesMax,
- &charIndex, &posX, &posY
- );
-
- label->dirty = false;
-}
-
-errorret_t uiLabelRender(uilabel_t *label, const color_t color) {
- uiLabelRebuffer(label);
- if(label->spriteCount == 0) errorOk();
-
- shadermaterial_t material = {
- .unlit = {
- .color = color,
- .texture = FONT_DEFAULT.texture
- }
- };
- errorChain(spriteBatchBuffer(
- label->sprites, label->spriteCount, &SHADER_UNLIT, material
- ));
-
- errorOk();
-}
-
-void uiLabelDispose(uilabel_t *label) {
- memoryZero(label, sizeof(uilabel_t));
-}
diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h
deleted file mode 100644
index edc16fab..00000000
--- a/src/dusk/ui/widget/uilabel.h
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "display/spritebatch/spritebatchsprite.h"
-#include "display/color.h"
-
-typedef struct {
- const char_t *text;
- spritebatchsprite_t *sprites;
- int32_t spritesMax;
- uint32_t spriteCount;
- int32_t width;
- int32_t height;
- float_t x;
- float_t y;
- bool_t dirty;
-
- // Wrapping - disabled while wrapWidth is 0 (the default). Enabling it
- // requires wrapBuffer, a caller-owned scratch buffer at least as large as
- // text: label->text is never written to directly (see uiLabelInit), so
- // uiLabelRebuffer instead copies it into wrapBuffer and word-wraps that
- // copy in place (via textWrap) before measuring/buffering it.
- char_t *wrapBuffer;
- uint32_t wrapBufferMax;
- float_t wrapWidth;
-} uilabel_t;
-
-/**
- * Initializes a label. The caller owns the text buffer and writes its
- * contents directly (setting label->dirty afterwards); call
- * uiLabelRebuffer or uiLabelRender to rebuild the cached sprites.
- *
- * @param label The label to initialize.
- * @param text Caller-owned, null-terminated string the label reads from
- * directly; never written to by the label. Cannot be NULL.
- * @param sprites Caller-owned sprite buffer the label writes cached
- * glyph sprites into. Cannot be NULL.
- * @param spritesMax Number of sprites the sprites buffer can hold.
- */
-void uiLabelInit(
- uilabel_t *label,
- const char_t *text,
- spritebatchsprite_t *sprites,
- const int32_t spritesMax
-);
-
-/**
- * Sets the screen x position of the label.
- *
- * @param label The label to update.
- * @param x Screen x position.
- */
-void uiLabelSetX(uilabel_t *label, const float_t x);
-
-/**
- * Returns the screen x position of the label.
- *
- * @param label The label to query.
- * @returns The label's x position.
- */
-float_t uiLabelGetX(const uilabel_t *label);
-
-/**
- * Sets the screen y position of the label.
- *
- * @param label The label to update.
- * @param y Screen y position.
- */
-void uiLabelSetY(uilabel_t *label, const float_t y);
-
-/**
- * Returns the screen y position of the label.
- *
- * @param label The label to query.
- * @returns The label's y position.
- */
-float_t uiLabelGetY(const uilabel_t *label);
-
-/**
- * Enables or disables word-wrap on the label and marks it dirty. Pass
- * wrapBuffer NULL to disable wrapping outright (wrapWidth is then
- * ignored); enabling it requires both a non-NULL wrapBuffer and a
- * wrapWidth greater than 0.
- *
- * @param label The label to configure.
- * @param wrapBuffer Caller-owned scratch buffer, at least as large as the
- * label's text, or NULL to disable wrapping.
- * @param wrapBufferMax Capacity of wrapBuffer, including the null
- * terminator.
- * @param wrapWidth Maximum line width in pixels; must be greater than 0
- * when wrapBuffer is non-NULL.
- */
-void uiLabelSetWrap(
- uilabel_t *label,
- char_t *wrapBuffer,
- const uint32_t wrapBufferMax,
- const float_t wrapWidth
-);
-
-/**
- * Rebuffers the label's cached sprites from its current text at its
- * current x/y position. No-op unless the label is dirty. Called
- * internally by uiLabelRender.
- *
- * @param label The label to rebuffer.
- */
-void uiLabelRebuffer(uilabel_t *label);
-
-/**
- * Renders the label's cached sprites at its current screen position.
- * Rebuffers first if the label is dirty (text or position changed).
- *
- * @param label The label to render.
- * @param color The color to render the text in.
- * @return Any error that occurs.
- */
-errorret_t uiLabelRender(uilabel_t *label, const color_t color);
-
-/**
- * Disposes of a label.
- *
- * @param label The label to dispose.
- */
-void uiLabelDispose(uilabel_t *label);
diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c
deleted file mode 100644
index 6737aff5..00000000
--- a/src/dusk/ui/widget/uimenu.c
+++ /dev/null
@@ -1,444 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uimenu.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "ui/widget/uibutton.h"
-#include "ui/widget/uitab.h"
-#include "ui/widget/uislider.h"
-#include "ui/widget/uidropdown.h"
-
-void uiMenuInit(
- uimenu_t *menu,
- uimenuselectedcallback_t selected,
- uimenuclosedcallback_t closed,
- uimenuchangedcallback_t changed
-) {
- assertNotNull(menu, "Menu cannot be NULL");
- memoryZero(menu, sizeof(uimenu_t));
- menu->selected = selected;
- menu->closed = closed;
- menu->changed = changed;
-}
-
-void uiMenuSetItems(
- uimenu_t *menu,
- const uimenuitem_t *items,
- const uint8_t itemCount,
- const uint8_t columns
-) {
- assertNotNull(menu, "Menu cannot be NULL");
- assertNotNull(items, "Items cannot be NULL");
- assertTrue(itemCount > 0, "Item count must be > 0");
- assertTrue(columns > 0, "Columns must be > 0");
- menu->items = (uimenuitem_t *)items;
- menu->itemCount = itemCount;
- menu->columns = columns;
-}
-
-void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y) {
- assertNotNull(menu, "Menu cannot be NULL");
- if(menu->focusItem == NULL) return;
- uiFocusSetPosition(menu->focusItem, x, y);
-}
-
-void uiMenuOpen(uimenu_t *menu) {
- assertNotNull(menu, "Menu cannot be NULL");
- assertNotNull(menu->items, "Menu items cannot be NULL");
- assertTrue(menu->itemCount > 0, "Menu item count must be > 0");
- assertTrue(menu->columns > 0, "Menu columns must be > 0");
- if(menu->focusItem != NULL) return;
-
- uint8_t focusable = uiMenuFocusableCount(menu);
- uint8_t rows = focusable > 0 ?
- (focusable + menu->columns - 1) / menu->columns : 1;
- menu->focusItem = uiFocusPush(
- menu->columns, rows,
- uiMenuFocusSelected,
- uiMenuFocusChanged,
- uiMenuFocusClosed,
- uiMenuFocusDirection,
- menu
- );
- menu->focusItem->disableBack = menu->disableBack;
- menu->focusItem->cancel = menu->cancel != NULL ? uiMenuFocusCancel : NULL;
-}
-
-void uiMenuClose(uimenu_t *menu) {
- assertNotNull(menu, "Menu cannot be NULL");
- if(menu->focusItem == NULL) return;
- uiFocusPopItem(menu->focusItem);
- menu->focusItem = NULL;
-}
-
-void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack) {
- assertNotNull(menu, "Menu cannot be NULL");
- menu->disableBack = disableBack;
- if(menu->focusItem != NULL) menu->focusItem->disableBack = disableBack;
-}
-
-void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel) {
- assertNotNull(menu, "Menu cannot be NULL");
- menu->cancel = cancel;
- if(menu->focusItem != NULL) {
- menu->focusItem->cancel = cancel != NULL ? uiMenuFocusCancel : NULL;
- }
-}
-
-bool_t uiMenuIsActive(const uimenu_t *menu) {
- assertNotNull(menu, "Menu cannot be NULL");
- return menu->focusItem != NULL;
-}
-
-errorret_t uiMenuDraw(
- const uimenu_t *menu,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-) {
- assertNotNull(menu, "Menu cannot be NULL");
- if(menu->itemCount == 0) errorOk();
-
- float_t colStep = width / (float_t)menu->columns;
- float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
-
- uint8_t col = 0;
- uint8_t row = 0;
-
- for(uint8_t i = 0; i < menu->itemCount; i++) {
- uimenuitem_t *item = &menu->items[i];
-
- if(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
- if(col > 0) { row++; col = 0; }
- errorChain(textDraw(
- x,
- y + (float_t)row * rowHeight,
- item->label,
- COLOR_WHITE,
- &FONT_DEFAULT
- ));
- row++;
- continue;
- }
-
- if(item->type == UI_MENU_WIDGET_TYPE_SPACER) {
- if(col > 0) { row++; col = 0; }
- row++;
- continue;
- }
-
- float_t ix = x + (float_t)col * colStep;
- float_t iy = y + (float_t)row * rowHeight;
-
- switch(item->type) {
- case UI_MENU_WIDGET_TYPE_CHECKBOX:
- errorChain(uiCheckboxDraw(&item->checkbox, ix, iy));
- break;
-
- case UI_MENU_WIDGET_TYPE_BUTTON:
- errorChain(uiButtonDraw(&item->button, ix, iy));
- break;
-
- case UI_MENU_WIDGET_TYPE_TAB:
- errorChain(uiTabDraw(&item->tab, ix, iy));
- break;
-
- case UI_MENU_WIDGET_TYPE_SLIDER:
- errorChain(uiSliderDraw(&item->slider, ix, iy));
- break;
-
- case UI_MENU_WIDGET_TYPE_DROPDOWN:
- errorChain(uiDropdownDraw(&item->dropdown, ix, iy));
- break;
-
- default:
- break;
- }
-
- col++;
- if(col >= menu->columns) { col = 0; row++; }
- }
-
- errorOk();
-}
-
-bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uimenu_t *menu = (uimenu_t *)focusItem->user;
- if(menu->selected == NULL) return true;
-
- uint8_t slot = focusItem->y * menu->columns + focusItem->x;
- uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
- if(index == 0xFF) return true;
-
- menu->selected(menu, index, &menu->items[index]);
- return true;
-}
-
-bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uimenu_t *menu = (uimenu_t *)focusItem->user;
-
- uint8_t focusSlot = focusItem->y * menu->columns + focusItem->x;
- uint8_t slot = 0;
-
- for(uint8_t i = 0; i < menu->itemCount; i++) {
- uimenuitem_t *item = &menu->items[i];
- if(
- item->type == UI_MENU_WIDGET_TYPE_LABEL ||
- item->type == UI_MENU_WIDGET_TYPE_SPACER
- ) continue;
- uiMenuItemSetHighlighted(item, slot == focusSlot);
- slot++;
- }
-
- if(menu->changed == NULL) return true;
- uint8_t index = uiMenuFocusSlotToIndex(menu, focusSlot);
- if(index == 0xFF) return true;
-
- menu->changed(menu, index, &menu->items[index]);
- return true;
-}
-
-bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uimenu_t *menu = (uimenu_t *)focusItem->user;
- menu->focusItem = NULL;
-
- for(uint8_t i = 0; i < menu->itemCount; i++) {
- uiMenuItemSetHighlighted(&menu->items[i], false);
- }
-
- if(menu->closed != NULL) menu->closed(menu);
- return true;
-}
-
-bool_t uiMenuFocusDirection(
- const uifocusitem_t *focusItem,
- const uifocusdirection_t direction
-) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uimenu_t *menu = (uimenu_t *)focusItem->user;
-
- if(
- direction == UI_FOCUS_DIRECTION_LEFT ||
- direction == UI_FOCUS_DIRECTION_RIGHT
- ) {
- uint8_t slot = focusItem->y * menu->columns + focusItem->x;
- uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
-
- if(index != 0xFF) {
- uimenuitem_t *item = &menu->items[index];
- bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT;
-
- if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) {
- if(right) {
- uiSliderStepUp(&item->slider);
- } else {
- uiSliderStepDown(&item->slider);
- }
- return true;
- }
-
- if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
- uiCheckboxToggle(&item->checkbox);
- return true;
- }
-
- if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) {
- if(right) {
- uiDropdownStepNext(&item->dropdown);
- } else {
- uiDropdownStepPrev(&item->dropdown);
- }
- return true;
- }
- }
- }
-
- return uiMenuFocusSkipBlanks(menu, focusItem, direction);
-}
-
-bool_t uiMenuItemIsBlank(const uimenuitem_t *item) {
- return
- item->type == UI_MENU_WIDGET_TYPE_BUTTON &&
- item->button.label.text[0] == '\0';
-}
-
-bool_t uiMenuFocusSkipBlanks(
- uimenu_t *menu,
- const uifocusitem_t *focusItem,
- const uifocusdirection_t direction
-) {
- int8_t dx = 0;
- int8_t dy = 0;
- for(
- const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
- m->action != INPUT_ACTION_NULL;
- m++
- ) {
- if(m->direction != direction) continue;
- dx = m->dx;
- dy = m->dy;
- break;
- }
- if(dx == 0 && dy == 0) return false;
-
- // Horizontal: a straight scan along the row. There's no "nearest"
- // ambiguity here - the next non-blank cell in the pressed direction is
- // always the right one, wrapping within the row as needed.
- if(dy == 0) {
- int16_t x = focusItem->x;
- for(uint8_t i = 0; i < menu->columns; i++) {
- x += dx;
- if(x < 0) x += menu->columns;
- x %= menu->columns;
-
- uint8_t slot = focusItem->y * menu->columns + (uint8_t)x;
- uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
- // Treat a missing item (label/spacer slot) the same as a blank
- // button - both get skipped over transparently.
- if(index != 0xFF && !uiMenuItemIsBlank(&menu->items[index])) {
- uiMenuSetPosition(menu, (uint8_t)x, focusItem->y);
- return true;
- }
- }
- return false;
- }
-
- // Vertical: search every cell strictly further in the pressed
- // direction (no wrap - the rows below are only ever "below", never
- // "above via the far edge") and land on whichever non-blank cell has
- // the smallest combined row+column distance from where we moved from.
- // This lets an exact column match several rows away beat an
- // approximate match one row away, while still reaching something like
- // SPACE from a key with nothing directly below it. Ties on total
- // distance favor the smaller column distance - e.g. moving straight
- // down two rows onto an exact column match beats landing one row down
- // one column over, even though both are three cells away.
- int16_t rowStart = dy > 0 ? focusItem->y + 1 : 0;
- int16_t rowEnd = dy > 0 ? focusItem->rows - 1 : focusItem->y - 1;
-
- bool_t found = false;
- uint16_t bestDistance = 0;
- uint8_t bestColDistance = 0;
- uint8_t bestX = 0;
- uint8_t bestY = 0;
-
- for(int16_t yy = rowStart; yy <= rowEnd; yy++) {
- uint8_t rowDistance = dy > 0 ?
- (uint8_t)(yy - focusItem->y) : (uint8_t)(focusItem->y - yy);
-
- for(uint8_t xx = 0; xx < menu->columns; xx++) {
- uint8_t colDistance = xx > focusItem->x ?
- xx - focusItem->x : focusItem->x - xx;
- uint16_t distance = rowDistance + colDistance;
- if(found && distance > bestDistance) continue;
- if(found && distance == bestDistance && colDistance >= bestColDistance) {
- continue;
- }
-
- uint8_t slot = (uint8_t)yy * menu->columns + xx;
- uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
- if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue;
-
- found = true;
- bestDistance = distance;
- bestColDistance = colDistance;
- bestX = xx;
- bestY = (uint8_t)yy;
- }
- }
-
- // Nothing further in that direction at all (already on the edge row) -
- // wrap to the opposite edge as a last resort, landing on its nearest
- // column. This only ever kicks in when the non-wrapped search above
- // found literally nothing, so it can't hijack a real nearby match the
- // way wrapping during the main search could.
- if(!found && focusItem->rows > 1) {
- uint8_t wrapY = dy > 0 ? 0 : focusItem->rows - 1;
- for(uint8_t xx = 0; xx < menu->columns; xx++) {
- uint8_t colDistance = xx > focusItem->x ?
- xx - focusItem->x : focusItem->x - xx;
- if(found && colDistance >= bestDistance) continue;
-
- uint8_t slot = wrapY * menu->columns + xx;
- uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
- if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue;
-
- found = true;
- bestDistance = colDistance;
- bestX = xx;
- bestY = wrapY;
- }
- }
-
- if(found) {
- uiMenuSetPosition(menu, bestX, bestY);
- return true;
- }
-
- // Nothing non-blank in that direction at all - fall through to the
- // default single-step move.
- return false;
-}
-
-bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem) {
- assertNotNull(focusItem, "Focus item cannot be NULL");
- assertNotNull(focusItem->user, "Focus item user cannot be NULL");
- uimenu_t *menu = (uimenu_t *)focusItem->user;
- if(menu->cancel == NULL) return false;
- return menu->cancel(menu);
-}
-
-uint8_t uiMenuFocusableCount(const uimenu_t *menu) {
- assertNotNull(menu, "Menu cannot be NULL");
- uint8_t count = 0;
- for(uint8_t i = 0; i < menu->itemCount; i++) {
- uimenuwidgettype_t t = menu->items[i].type;
- if(t != UI_MENU_WIDGET_TYPE_LABEL && t != UI_MENU_WIDGET_TYPE_SPACER) {
- count++;
- }
- }
- return count;
-}
-
-uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) {
- assertNotNull(menu, "Menu cannot be NULL");
- uint8_t focusable = 0;
- for(uint8_t i = 0; i < menu->itemCount; i++) {
- uimenuwidgettype_t t = menu->items[i].type;
- if(t == UI_MENU_WIDGET_TYPE_LABEL || t == UI_MENU_WIDGET_TYPE_SPACER) {
- continue;
- }
- if(focusable == slot) return i;
- focusable++;
- }
- return 0xFF;
-}
-
-void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) {
- if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
- uiCheckboxSetHighlighted(&item->checkbox, highlighted);
- } else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) {
- uiButtonSetHighlighted(&item->button, highlighted);
- } else if(item->type == UI_MENU_WIDGET_TYPE_TAB) {
- uiTabSetActive(&item->tab, highlighted);
- } else if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) {
- uiSliderSetHighlighted(&item->slider, highlighted);
- } else if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) {
- uiDropdownSetHighlighted(&item->dropdown, highlighted);
- }
-}
diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h
deleted file mode 100644
index 88abc38f..00000000
--- a/src/dusk/ui/widget/uimenu.h
+++ /dev/null
@@ -1,386 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/focus/uifocus.h"
-#include "ui/widget/uibutton.h"
-#include "ui/widget/uicheckbox.h"
-#include "ui/widget/uitab.h"
-#include "ui/widget/uislider.h"
-#include "ui/widget/uidropdown.h"
-
-#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX
-
-typedef struct uimenu_s uimenu_t;
-
-typedef enum {
- UI_MENU_WIDGET_TYPE_NONE,
- UI_MENU_WIDGET_TYPE_LABEL,
- UI_MENU_WIDGET_TYPE_SPACER,
- UI_MENU_WIDGET_TYPE_CHECKBOX,
- UI_MENU_WIDGET_TYPE_BUTTON,
- UI_MENU_WIDGET_TYPE_TAB,
- UI_MENU_WIDGET_TYPE_SLIDER,
- UI_MENU_WIDGET_TYPE_DROPDOWN,
-} uimenuwidgettype_t;
-
-typedef struct {
- uimenuwidgettype_t type;
- union {
- char_t *label;
- uicheckbox_t checkbox;
- uibutton_t button;
- uitab_t tab;
- uislider_t slider;
- uidropdown_t dropdown;
- };
-} uimenuitem_t;
-
-typedef void (*uimenuselectedcallback_t)(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-);
-
-typedef void (*uimenuchangedcallback_t)(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-);
-
-typedef void (*uimenuclosedcallback_t)(const uimenu_t *menu);
-
-/**
- * Callback invoked when INPUT_ACTION_CANCEL is pressed while the menu is
- * focused - see uiMenuSetCancelCallback.
- *
- * @param menu The menu that was cancelled.
- * @returns True if fully handled (skips the default disableBack/pop
- * behavior); false to fall through to it.
- */
-typedef bool_t (*uimenucancelcallback_t)(const uimenu_t *menu);
-
-typedef struct uimenu_s {
- uimenuitem_t *items;
- uint8_t itemCount;
- uint8_t columns;
- uifocusitem_t *focusItem;
-
- uimenuselectedcallback_t selected;
- uimenuclosedcallback_t closed;
- uimenuchangedcallback_t changed;
- uimenucancelcallback_t cancel;
-
- void *user;
-
- // See uiMenuSetDisableBack.
- bool_t disableBack;
-} uimenu_t;
-
-/**
- * Initializes a menu, clearing all items and focus state.
- *
- * @param menu The menu to initialize.
- * @param items The list of items to display in the menu.
- * @param itemCount The number of items in the list.
- * @param columns The number of columns to display the items in.
- * @param selected The callback to invoke when an item is selected.
- * @param closed The callback to invoke when the menu is closed.
- * @param changed The callback to invoke when the menu changes.
- */
-void uiMenuInit(
- uimenu_t *menu,
- uimenuselectedcallback_t selected,
- uimenuclosedcallback_t closed,
- uimenuchangedcallback_t changed
-);
-
-/**
- * Sets the items to display in the menu.
- *
- * @param menu The menu to update.
- * @param items The list of items to display in the menu.
- * @param itemCount The number of items in the list.
- * @param columns The number of columns to display the items in.
- */
-void uiMenuSetItems(
- uimenu_t *menu,
- const uimenuitem_t *items,
- const uint8_t itemCount,
- const uint8_t columns
-);
-
-/**
- * Sets the position of the menu on the screen.
- *
- * @param menu The menu to position.
- * @param x The x-coordinate to position the menu at.
- * @param y The y-coordinate to position the menu at.
- */
-void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y);
-
-/**
- * Pushes a menu onto the UI focus stack, making it the active menu.
- *
- * @param menu The menu to push.
- */
-void uiMenuOpen(uimenu_t *menu);
-
-/**
- * Pops a menu from the UI focus stack, removing it from the active menu.
- *
- * @param menu The menu to pop.
- */
-void uiMenuClose(uimenu_t *menu);
-
-/**
- * Sets whether pressing back/cancel is ignored while this menu is the
- * topmost focus item - it simply does nothing, rather than closing the
- * menu. Does not affect a deliberate uiMenuClose() call. Safe to call
- * before or after uiMenuOpen().
- *
- * @param menu The menu to update.
- * @param disableBack True to ignore back/cancel input for this menu.
- */
-void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack);
-
-/**
- * Sets a callback given first refusal of INPUT_ACTION_CANCEL while this
- * menu is the topmost focus item, before disableBack/the default pop is
- * applied. Safe to call before or after uiMenuOpen().
- *
- * @param menu The menu to update.
- * @param cancel The callback to install, or NULL to remove it and
- * restore default disableBack/pop behavior.
- */
-void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel);
-
-/**
- * Returns whether the menu is currently active (on the UI focus stack).
- *
- * @param menu The menu to query.
- * @returns True if the menu is active.
- */
-bool_t uiMenuIsActive(const uimenu_t *menu);
-
-/**
- * Draws the menu at the specified position and size.
- *
- * @param menu The menu to draw.
- * @param x The x-coordinate to draw the menu at.
- * @param y The y-coordinate to draw the menu at.
- * @param width The width of the menu.
- * @param height The height of the menu.
- * @returns An error code indicating success or failure.
- */
-errorret_t uiMenuDraw(
- const uimenu_t *menu,
- const float_t x,
- const float_t y,
- const float_t width,
- const float_t height
-);
-
-/**
- * Returns the number of focusable (non-label) items in the menu.
- *
- * @param menu The menu to query.
- * @returns Count of non-label items.
- */
-uint8_t uiMenuFocusableCount(const uimenu_t *menu);
-
-/**
- * Maps a flat focus slot index to the corresponding item array index,
- * skipping over label items which are not focusable.
- *
- * @param menu The menu to query.
- * @param slot The focus slot index (y * columns + x).
- * @returns The item array index, or 0xFF if out of range.
- */
-uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot);
-
-/**
- * Sets the highlighted/active state on a menu item, dispatching to the
- * matching widget's setter based on its type. No-op for label/spacer
- * items and any other type with no highlight concept.
- *
- * @param item The item to update.
- * @param highlighted The new highlighted/active state.
- */
-void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted);
-
-/**
- * Internal focus callback - forwards selection to the menu's selected
- * handler.
- *
- * @param focusItem The active focus item; user field must point to uimenu_t.
- * @returns True.
- */
-bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem);
-
-/**
- * Internal focus callback - updates widget highlights and fires changed.
- *
- * @param focusItem The active focus item; user field must point to uimenu_t.
- * @returns True.
- */
-bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem);
-
-/**
- * Internal focus callback - clears focusItem and fires the closed handler.
- *
- * @param focusItem The active focus item; user field must point to uimenu_t.
- * @returns True.
- */
-bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem);
-
-/**
- * Internal focus callback - gives the focused item's widget a chance to
- * handle LEFT/RIGHT itself (e.g. a slider adjusting its value); failing
- * that, moves to the next non-blank cell in the given direction (see
- * uiMenuFocusSkipBlanks) rather than the default single-step move, which
- * would happily land on a deliberately-blank layout cell (e.g. a
- * UI_MENU_WIDGET_TYPE_BUTTON with an empty label, used as filler).
- *
- * @param focusItem The active focus item; user field must point to uimenu_t.
- * @param direction The direction that was pressed or held.
- * @returns True if handled (a widget adjusted itself, or focus moved to
- * a non-blank cell); false to fall through to the default single-step
- * move (only when every cell along this line is blank).
- */
-bool_t uiMenuFocusDirection(
- const uifocusitem_t *focusItem,
- const uifocusdirection_t direction
-);
-
-/**
- * True for a menu item that should be transparently skipped during
- * d-pad navigation - a UI_MENU_WIDGET_TYPE_BUTTON with an empty label,
- * used purely to hold a grid cell's position (the menu/focus grid has no
- * concept of a row narrower than its column count, so short rows are
- * padded with these instead). Labels/spacers are unaffected by this -
- * they're already excluded from the focus grid entirely.
- *
- * @param item The item to check.
- * @returns True if this item is blank filler.
- */
-bool_t uiMenuItemIsBlank(const uimenuitem_t *item);
-
-/**
- * Moves focusItem to a non-blank cell (see uiMenuItemIsBlank) in the
- * given direction, wrapping the grid as many times as needed - so, from
- * the menu's perspective, a blank cell simply isn't there.
- *
- * LEFT/RIGHT scan straight along the row, since there's no ambiguity
- * about which non-blank cell is "next" (wrapping within the row as
- * needed). UP/DOWN instead considers every cell strictly further in the
- * pressed direction and lands on whichever non-blank cell has the
- * smallest combined row+column distance from the one moved from - e.g.
- * moving down off a densely-packed row onto a mostly-blank one lands on
- * whatever's closest instead of only ever reaching a cell that happens
- * to share the exact same column, while an exact column match a couple
- * of rows down still wins over an approximate match one row down. Ties
- * on total distance favor the smaller column distance, so a straight
- * move down onto an exact column match beats an equally-far diagonal
- * one. This search never wraps in either axis while there's still a
- * real cell further in the pressed direction - column/row 0 is never
- * treated as adjacent to the far edge just because the grid wraps in
- * general. Only once already on the edge row (nothing further to search
- * at all) does it fall back to wrapping to the opposite edge's nearest
- * column, so pressing up from the top row still goes somewhere instead
- * of doing nothing.
- *
- * Gives up (returning false, so the default single-step move applies
- * instead) only if every cell on the grid is blank.
- *
- * @param menu The menu to move within.
- * @param focusItem The active focus item; must belong to menu.
- * @param direction The direction that was pressed or held.
- * @returns True if a non-blank cell was found and focus moved there.
- */
-bool_t uiMenuFocusSkipBlanks(
- uimenu_t *menu,
- const uifocusitem_t *focusItem,
- const uifocusdirection_t direction
-);
-
-/**
- * Internal focus callback - forwards INPUT_ACTION_CANCEL to the menu's
- * cancel handler, if one is set.
- *
- * @param focusItem The active focus item; user field must point to uimenu_t.
- * @returns The cancel handler's result, or false if none is set.
- */
-bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem);
-
-// Helper macros
-#define MENU_BEGIN(menuPtr, itemsArray, selected, closed, changed) \
- uimenu_t *menu = (menuPtr); \
- uiMenuInit(menu, selected, closed, changed); \
- menu->items = (itemsArray); \
- uint8_t menuIndex = 0; \
- uint8_t menuCapacity = sizeof(itemsArray) / sizeof((itemsArray)[0])
-
-#define MENU_LABEL(text) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \
- menu->items[menuIndex].label = text; \
- ++menuIndex
-
-#define MENU_SPACER() \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SPACER; \
- ++menuIndex
-
-#define MENU_CHECKBOX(label) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_CHECKBOX; \
- uiCheckboxInit(&menu->items[menuIndex].checkbox, label); \
- ++menuIndex
-
-#define MENU_BUTTON(label) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_BUTTON; \
- uiButtonInit(&menu->items[menuIndex].button, label); \
- ++menuIndex
-
-#define MENU_TAB(label) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_TAB; \
- uiTabInit(&menu->items[menuIndex].tab, label); \
- ++menuIndex
-
-#define MENU_SLIDER_FLOAT(label, value, min, max, step) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \
- uiSliderInitFloat( \
- &menu->items[menuIndex].slider, label, value, min, max, step \
- ); \
- ++menuIndex
-
-#define MENU_SLIDER_INT(label, value, min, max, step) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \
- uiSliderInitInt( \
- &menu->items[menuIndex].slider, label, value, min, max, step \
- ); \
- ++menuIndex
-
-#define MENU_DROPDOWN(label, options, optionCount, selectedIndex) \
- assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
- menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_DROPDOWN; \
- uiDropdownInit( \
- &menu->items[menuIndex].dropdown, label, options, optionCount, \
- selectedIndex \
- ); \
- ++menuIndex
-
-#define MENU_END(itemsArray, columns) \
- uiMenuSetItems(menu, (itemsArray), menuIndex, (columns))
-
-//EOF
diff --git a/src/dusk/ui/widget/uimodal.c b/src/dusk/ui/widget/uimodal.c
deleted file mode 100644
index 9276f26b..00000000
--- a/src/dusk/ui/widget/uimodal.c
+++ /dev/null
@@ -1,264 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uimodal.h"
-#include "ui/widget/uiframe.h"
-#include "ui/focus/uifocus.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/string.h"
-#include "util/math.h"
-#include "display/screen/screen.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/texture/texture.h"
-#include "display/shader/shaderunlit.h"
-#include "locale/localemanager.h"
-#include "asset/loader/locale/assetlocaleloader.h"
-
-#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160)
-
-uimodal_t UI_MODAL;
-
-// Focus item backing a message-only modal (no options) - lets a plain
-// MODAL still block on player confirmation. NULL whenever a
-// message-only modal isn't open. Mirrors uitextboxmain.c's pattern.
-static uifocusitem_t *messageFocus = NULL;
-
-void uiModalSelected(
- const uimenu_t *menu,
- const uint8_t index,
- const uimenuitem_t *item
-) {
- UI_MODAL.result = index;
- uiModalClose(NULL);
-}
-
-void uiModalClosed(const uimenu_t *menu) {
- if(UI_MODAL.callback != NULL) {
- UI_MODAL.callback(UI_MODAL.result, UI_MODAL.user);
- }
-}
-
-bool_t uiModalMessageFocusSelected(const uifocusitem_t *item) {
- uiFocusPopItem(messageFocus);
- return true;
-}
-
-bool_t uiModalMessageFocusClosed(const uifocusitem_t *item) {
- messageFocus = NULL;
- UI_MODAL.open = false;
- uiModalClosed(NULL);
- return true;
-}
-
-errorret_t uiModalInit(void) {
- memoryZero(&UI_MODAL, sizeof(uimodal_t));
-
- uiLabelInit(
- &UI_MODAL.titleLabel, UI_MODAL.titleText,
- UI_MODAL.titleSprites, UI_MODAL_TITLE_SPRITES_MAX
- );
- uiLabelInit(
- &UI_MODAL.messageLabel, UI_MODAL.messageText,
- UI_MODAL.messageSprites, UI_MODAL_MESSAGE_SPRITES_MAX
- );
-
- errorOk();
-}
-
-errorret_t uiModalDraw(void) {
- if(!UI_MODAL.open) errorOk();
-
- bool_t hasOptions = UI_MODAL.menu.itemCount > 0;
-
- spritebatchsprite_t backdropSprite = {
- .min = { 0.0f, 0.0f, 0.0f },
- .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t backdropMaterial = {
- .unlit = {
- .color = UI_MODAL_BACKDROP_COLOR,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(
- spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
- );
- errorChain(spriteBatchFlush());
-
- float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
- float_t contentWidth = mathMax(
- mathMax(
- (float_t)UI_MODAL.titleLabel.width, (float_t)UI_MODAL.messageLabel.width
- ),
- UI_MODAL_MIN_WIDTH - (UI_FRAME_START_X * 2)
- );
- float_t width = contentWidth + (UI_FRAME_START_X * 2);
- float_t height = (UI_FRAME_START_Y * 2)
- + (UI_MODAL.hasTitle ?
- (float_t)UI_MODAL.titleLabel.height + UI_FRAME_PADDING_Y : 0.0f)
- + (float_t)UI_MODAL.messageLabel.height
- + (hasOptions ? UI_FRAME_PADDING_Y + rowHeight : 0.0f);
-
- float_t x = (float_t)SCREEN.scanX +
- ((float_t)SCREEN.scanWidth - width) * 0.5f;
- float_t y = (float_t)SCREEN.scanY +
- ((float_t)SCREEN.scanHeight - height) * 0.5f;
-
- errorChain(uiFrameDraw(x, y, width, height));
-
- float_t contentX = x + UI_FRAME_START_X;
- float_t contentY = y + UI_FRAME_START_Y;
-
- float_t messageY = contentY;
- if(UI_MODAL.hasTitle) {
- uiLabelSetX(&UI_MODAL.titleLabel, contentX);
- uiLabelSetY(&UI_MODAL.titleLabel, contentY);
- errorChain(uiLabelRender(&UI_MODAL.titleLabel, COLOR_WHITE));
-
- messageY = contentY + (float_t)UI_MODAL.titleLabel.height +
- UI_FRAME_PADDING_Y;
- }
-
- uiLabelSetX(&UI_MODAL.messageLabel, contentX);
- uiLabelSetY(&UI_MODAL.messageLabel, messageY);
- errorChain(uiLabelRender(&UI_MODAL.messageLabel, COLOR_WHITE));
-
- if(hasOptions) {
- float_t buttonsY = messageY + (float_t)UI_MODAL.messageLabel.height +
- UI_FRAME_PADDING_Y;
- errorChain(
- uiMenuDraw(&UI_MODAL.menu, contentX, buttonsY, contentWidth, rowHeight)
- );
- }
-
- errorChain(spriteBatchFlush());
- errorOk();
-}
-
-void uiModalLocalize(
- const char_t *key,
- char_t *buffer,
- const size_t bufferSize
-) {
- if(key[0] == '\0') {
- buffer[0] = '\0';
- return;
- }
-
- errorret_t result = localeManagerGetString(key, buffer, bufferSize, 0);
- if(errorIsNotOk(result)) {
- errorCatch(result);
- stringCopy(buffer, key, bufferSize - 1);
- }
-}
-
-bool_t uiModalIsOpen(void) {
- return UI_MODAL.open;
-}
-
-uint8_t uiModalGetResult(void) {
- return UI_MODAL.result;
-}
-
-void uiModalOpen(
- const char_t *title,
- const char_t *message,
- const char_t **options,
- const uint8_t optionCount,
- uimodaloptioncallback_t callback,
- uimodalopenedcallback_t onOpen,
- void *user
-) {
- assertNotNull(message, "Message cannot be NULL");
- assertTrue(
- options != NULL || optionCount == 0,
- "Options cannot be NULL when optionCount > 0"
- );
- assertTrue(
- optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal"
- );
-
- float_t maxContentWidth =
- ((float_t)SCREEN.scanWidth * UI_MODAL_MAX_WIDTH_RATIO)
- - (UI_FRAME_START_X * 2.0f);
-
- UI_MODAL.hasTitle = title != NULL;
- if(UI_MODAL.hasTitle) {
- uiModalLocalize(title, UI_MODAL.titleText, UI_MODAL_TITLE_TEXT_MAX);
- textWrap(UI_MODAL.titleText, &FONT_DEFAULT, maxContentWidth);
- } else {
- UI_MODAL.titleText[0] = '\0';
- }
- UI_MODAL.titleLabel.dirty = true;
- uiLabelRebuffer(&UI_MODAL.titleLabel);
-
- uiModalLocalize(message, UI_MODAL.messageText, UI_MODAL_MESSAGE_TEXT_MAX);
- textWrap(UI_MODAL.messageText, &FONT_DEFAULT, maxContentWidth);
- UI_MODAL.messageLabel.dirty = true;
- uiLabelRebuffer(&UI_MODAL.messageLabel);
-
- UI_MODAL.callback = callback;
- UI_MODAL.onOpen = onOpen;
- UI_MODAL.user = user;
- UI_MODAL.result = UI_MODAL_RESULT_NONE;
- UI_MODAL.open = true;
-
- if(options != NULL) {
- MENU_BEGIN(
- &UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL
- );
- for(uint8_t i = 0; i < optionCount; i++) {
- uiModalLocalize(
- options[i], UI_MODAL.optionsText[i], UI_MODAL_OPTION_TEXT_MAX
- );
- MENU_BUTTON(UI_MODAL.optionsText[i]);
- }
- MENU_END(UI_MODAL.options, menuIndex);
-
- // A dialog with options must be dismissed by picking one, not by
- // backing out of it - see uiMenuSetDisableBack.
- uiMenuSetDisableBack(&UI_MODAL.menu, true);
- uiMenuOpen(&UI_MODAL.menu);
- } else {
- uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
- messageFocus = uiFocusPush(
- 1, 1,
- uiModalMessageFocusSelected, NULL, uiModalMessageFocusClosed,
- NULL, NULL
- );
- }
-
- if(UI_MODAL.onOpen != NULL) {
- UI_MODAL.onOpen(UI_MODAL.user);
- }
-}
-
-void uiModalClose(uimodalclosedcallback_t callback) {
- if(!UI_MODAL.open) return;
- UI_MODAL.open = false;
-
- if(uiMenuIsActive(&UI_MODAL.menu)) {
- uiMenuClose(&UI_MODAL.menu);
- } else if(messageFocus != NULL) {
- uiFocusPopItem(messageFocus);
- } else {
- uiModalClosed(NULL);
- }
-
- if(callback != NULL) {
- callback(UI_MODAL.user);
- }
-}
-
-errorret_t uiModalDispose(void) {
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uimodal.h b/src/dusk/ui/widget/uimodal.h
deleted file mode 100644
index 2a10b13f..00000000
--- a/src/dusk/ui/widget/uimodal.h
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-#include "ui/widget/uilabel.h"
-#include "ui/widget/uimenu.h"
-
-#define UI_MODAL_TITLE_TEXT_MAX 64
-#define UI_MODAL_TITLE_SPRITES_MAX UI_MODAL_TITLE_TEXT_MAX
-#define UI_MODAL_MESSAGE_TEXT_MAX 256
-#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
-#define UI_MODAL_OPTIONS_MAX 4
-#define UI_MODAL_OPTION_TEXT_MAX 32
-#define UI_MODAL_MIN_WIDTH 160.0f
-// Title/message text wraps at word boundaries to stay within this
-// fraction of the screen's scan width, so long (e.g. translated)
-// strings don't run off-screen.
-#define UI_MODAL_MAX_WIDTH_RATIO 0.8f
-#define UI_MODAL_RESULT_NONE 0xFF
-
-/**
- * Callback invoked once a modal is dismissed.
- *
- * @param optionIndex Index into the options array passed to uiModalOpen
- * that was selected. A dialog with options ignores back/cancel input
- * (see uiMenuSetDisableBack) and must be dismissed by picking one, so
- * UI_MODAL_RESULT_NONE only reaches here via a direct uiModalClose()
- * call made before any option was picked.
- * @param user Arbitrary pointer passed to uiModalOpen.
- */
-typedef void (*uimodaloptioncallback_t)(const uint8_t optionIndex, void *user);
-
-/**
- * Callback invoked when a modal opens. Fired immediately for now, but
- * gives a hook point for a future open transition to key off of.
- *
- * @param user Arbitrary pointer passed to uiModalOpen.
- */
-typedef void (*uimodalopenedcallback_t)(void *user);
-
-/**
- * Callback invoked when a modal closes, passed to uiModalClose.
- *
- * @param user Arbitrary pointer passed to uiModalOpen.
- */
-typedef void (*uimodalclosedcallback_t)(void *user);
-
-typedef struct {
- uilabel_t titleLabel;
- char_t titleText[UI_MODAL_TITLE_TEXT_MAX];
- spritebatchsprite_t titleSprites[UI_MODAL_TITLE_SPRITES_MAX];
-
- uilabel_t messageLabel;
- char_t messageText[UI_MODAL_MESSAGE_TEXT_MAX];
- spritebatchsprite_t messageSprites[UI_MODAL_MESSAGE_SPRITES_MAX];
-
- uimenu_t menu;
- uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
- char_t optionsText[UI_MODAL_OPTIONS_MAX][UI_MODAL_OPTION_TEXT_MAX];
-
- uimodaloptioncallback_t callback;
- uimodalopenedcallback_t onOpen;
- void *user;
- uint8_t result;
- bool_t open;
- bool_t hasTitle;
-} uimodal_t;
-
-extern uimodal_t UI_MODAL;
-
-/**
- * Initializes the modal dialog.
- *
- * @return Any error that occurs.
- */
-errorret_t uiModalInit(void);
-
-/**
- * Draws the modal dialog: a semi-transparent black backdrop covering the
- * whole screen, then its own centered frame with the title, message, and
- * option buttons. No-op when not open.
- *
- * @return Any error that occurs.
- */
-errorret_t uiModalDraw(void);
-
-/**
- * Returns true when the modal dialog is currently open.
- *
- * @returns True if open.
- */
-bool_t uiModalIsOpen(void);
-
-/**
- * Returns the result of the most recently dismissed modal.
- *
- * @returns The selected option index, or UI_MODAL_RESULT_NONE.
- */
-uint8_t uiModalGetResult(void);
-
-/**
- * Opens the modal dialog with the given title, message, and options.
- * callback is invoked exactly once with the selected option. A dialog
- * with options > 0 disables back/cancel (see uiMenuSetDisableBack), so
- * it must be dismissed by picking one.
- *
- * title, message, and each option are treated as locale message IDs
- * first: if one matches an entry in the active locale file its
- * translated string is used, otherwise the string itself is displayed
- * verbatim. See uiModalLocalize.
- *
- * @param title Display title, or a locale message ID; copied
- * internally, safe to be transient. May be NULL for a dialog with no
- * title row.
- * @param message Display message, or a locale message ID; copied
- * internally, safe to be transient.
- * @param options Array of option label strings/locale message IDs;
- * copied internally, safe to be transient. May be NULL for a
- * message-only dialog with no option buttons, in which case optionCount
- * must be 0.
- * @param optionCount Number of options, from 0 to UI_MODAL_OPTIONS_MAX.
- * @param callback Called with the result once the dialog closes. May be
- * NULL.
- * @param onOpen Called once the modal has opened. May be NULL.
- * @param user Arbitrary pointer passed through to callback and onOpen,
- * and to whatever callback is later passed to uiModalClose.
- */
-void uiModalOpen(
- const char_t *title,
- const char_t *message,
- const char_t **options,
- const uint8_t optionCount,
- uimodaloptioncallback_t callback,
- uimodalopenedcallback_t onOpen,
- void *user
-);
-
-/**
- * Closes the modal dialog. No-op when already closed.
- *
- * @param callback Called with the modal's user pointer once the dialog
- * has closed, after the option-result callback set by uiModalOpen. May
- * be NULL. Not called if the modal was already closed.
- */
-void uiModalClose(uimodalclosedcallback_t callback);
-
-/**
- * Disposes of the modal dialog.
- *
- * @return Any error that occurs.
- */
-errorret_t uiModalDispose(void);
-
-/**
- * Resolves display text for uiModalOpen: if key matches a locale
- * message ID in the active locale file, buffer is filled with its
- * translated string, otherwise key is copied into buffer verbatim. An
- * empty key (e.g. an omitted JSON title/message, which defaults to ""
- * rather than NULL - see cutscenemodal_t/cutscenemodaloptionsmarkers_t)
- * is left as an empty buffer rather than looked up, since
- * assetLocaleGetString rejects an empty message ID outright.
- *
- * @param key Display string or locale message ID.
- * @param buffer Destination buffer.
- * @param bufferSize Size of buffer, in bytes.
- */
-void uiModalLocalize(
- const char_t *key,
- char_t *buffer,
- const size_t bufferSize
-);
diff --git a/src/dusk/ui/widget/uiscrolling.c b/src/dusk/ui/widget/uiscrolling.c
deleted file mode 100644
index 516caf00..00000000
--- a/src/dusk/ui/widget/uiscrolling.c
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uiscrolling.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-
-void uiScrollingInit(uiscrolling_t *scrolling) {
- assertNotNull(scrolling, "Scrolling container cannot be NULL");
- memoryZero(scrolling, sizeof(uiscrolling_t));
-}
-
-void uiScrollingEnsureVisible(
- uiscrolling_t *scrolling,
- const uint8_t index,
- const uint8_t itemCount,
- const uint8_t visibleCount
-) {
- assertNotNull(scrolling, "Scrolling container cannot be NULL");
- assertTrue(visibleCount > 0, "Visible count must be > 0");
-
- if(index < scrolling->offset) {
- scrolling->offset = index;
- } else if(index >= (uint8_t)(scrolling->offset + visibleCount)) {
- scrolling->offset = (uint8_t)(index - visibleCount + 1);
- }
-
- uint8_t maxOffset = itemCount > visibleCount ? itemCount - visibleCount : 0;
- if(scrolling->offset > maxOffset) scrolling->offset = maxOffset;
-}
diff --git a/src/dusk/ui/widget/uiscrolling.h b/src/dusk/ui/widget/uiscrolling.h
deleted file mode 100644
index da47729a..00000000
--- a/src/dusk/ui/widget/uiscrolling.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct {
- uint8_t offset;
-} uiscrolling_t;
-
-/**
- * Initializes a scrolling container with a zero offset.
- *
- * @param scrolling The scrolling container to initialize.
- */
-void uiScrollingInit(uiscrolling_t *scrolling);
-
-/**
- * Scrolls by the minimum amount needed to bring index within the visible
- * window of visibleCount rows, then clamps so the window never scrolls
- * past the end of itemCount rows.
- *
- * @param scrolling The scrolling container to update.
- * @param index Row index that must end up visible.
- * @param itemCount Total number of rows in the list.
- * @param visibleCount Number of rows visible at once.
- */
-void uiScrollingEnsureVisible(
- uiscrolling_t *scrolling,
- const uint8_t index,
- const uint8_t itemCount,
- const uint8_t visibleCount
-);
diff --git a/src/dusk/ui/widget/uislider.c b/src/dusk/ui/widget/uislider.c
deleted file mode 100644
index 5b30bfe9..00000000
--- a/src/dusk/ui/widget/uislider.c
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uislider.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "util/math.h"
-#include "util/string.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/texture/texture.h"
-#include "display/shader/shaderunlit.h"
-
-void uiSliderInitFloat(
- uislider_t *slider,
- const char_t *label,
- const float_t value,
- const float_t min,
- const float_t max,
- const float_t step
-) {
- assertNotNull(slider, "Slider cannot be NULL");
- assertNotNull(label, "Label cannot be NULL");
- assertTrue(max > min, "Slider max must be greater than min");
- assertTrue(step > 0.0f, "Slider step must be greater than zero");
-
- memoryZero(slider, sizeof(uislider_t));
- slider->label = label;
- slider->type = UI_SLIDER_TYPE_FLOAT;
- slider->min.f = min;
- slider->max.f = max;
- slider->step.f = step;
- slider->value.f = mathClamp(value, min, max);
-}
-
-void uiSliderInitInt(
- uislider_t *slider,
- const char_t *label,
- const int32_t value,
- const int32_t min,
- const int32_t max,
- const int32_t step
-) {
- assertNotNull(slider, "Slider cannot be NULL");
- assertNotNull(label, "Label cannot be NULL");
- assertTrue(max > min, "Slider max must be greater than min");
- assertTrue(step > 0, "Slider step must be greater than zero");
-
- memoryZero(slider, sizeof(uislider_t));
- slider->label = label;
- slider->type = UI_SLIDER_TYPE_INT;
- slider->min.i = min;
- slider->max.i = max;
- slider->step.i = step;
- slider->value.i = mathClamp(value, min, max);
-}
-
-float_t uiSliderGetFloat(const uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- return slider->type == UI_SLIDER_TYPE_INT ?
- (float_t)slider->value.i : slider->value.f;
-}
-
-int32_t uiSliderGetInt(const uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- assertTrue(
- slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
- );
- return slider->value.i;
-}
-
-void uiSliderSetFloat(uislider_t *slider, const float_t value) {
- assertNotNull(slider, "Slider cannot be NULL");
- assertTrue(
- slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider"
- );
- slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
-}
-
-void uiSliderSetInt(uislider_t *slider, const int32_t value) {
- assertNotNull(slider, "Slider cannot be NULL");
- assertTrue(
- slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
- );
- slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
-}
-
-void uiSliderStepUp(uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- if(slider->type == UI_SLIDER_TYPE_INT) {
- int32_t next = slider->value.i + slider->step.i;
- uiSliderSetInt(slider, next > slider->max.i ? slider->min.i : next);
- } else {
- float_t next = slider->value.f + slider->step.f;
- uiSliderSetFloat(slider, next > slider->max.f ? slider->min.f : next);
- }
-}
-
-void uiSliderStepDown(uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- if(slider->type == UI_SLIDER_TYPE_INT) {
- int32_t next = slider->value.i - slider->step.i;
- uiSliderSetInt(slider, next < slider->min.i ? slider->max.i : next);
- } else {
- float_t next = slider->value.f - slider->step.f;
- uiSliderSetFloat(slider, next < slider->min.f ? slider->max.f : next);
- }
-}
-
-float_t uiSliderGetRatio(const uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- if(slider->type == UI_SLIDER_TYPE_INT) {
- if(slider->max.i == slider->min.i) return 0.0f;
- return (float_t)(slider->value.i - slider->min.i) /
- (float_t)(slider->max.i - slider->min.i);
- }
-
- if(slider->max.f == slider->min.f) return 0.0f;
- return (slider->value.f - slider->min.f) / (slider->max.f - slider->min.f);
-}
-
-int32_t uiSliderGetStepCount(const uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- if(slider->type != UI_SLIDER_TYPE_INT || slider->step.i <= 0) return 0;
- return (slider->max.i - slider->min.i) / slider->step.i;
-}
-
-bool_t uiSliderIsHighlighted(const uislider_t *slider) {
- assertNotNull(slider, "Slider cannot be NULL");
- return slider->highlighted;
-}
-
-void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) {
- assertNotNull(slider, "Slider cannot be NULL");
- slider->highlighted = highlighted;
-}
-
-errorret_t uiSliderDraw(
- const uislider_t *slider,
- const float_t x,
- const float_t y
-) {
- assertNotNull(slider, "Slider cannot be NULL");
-
- color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
-
- errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT));
-
- int32_t labelW, labelH;
- textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH);
-
- float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP;
- float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
-
- spritebatchsprite_t trackSprite = {
- .min = { trackX, trackY, 0.0f },
- .max = {
- trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f
- },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t trackMaterial = {
- .unlit = {
- .color = COLOR_DARK_GRAY,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
- errorChain(spriteBatchFlush());
-
- float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
- if(fillWidth > 0.0f) {
- spritebatchsprite_t fillSprite = {
- .min = { trackX, trackY, 0.0f },
- .max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t fillMaterial = {
- .unlit = {
- .color = color,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
- errorChain(spriteBatchFlush());
- }
-
- int32_t stepCount = uiSliderGetStepCount(slider);
- if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
- for(int32_t i = 0; i <= stepCount; i++) {
- float_t markerX = trackX +
- UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
- spritebatchsprite_t markerSprite = {
- .min = {
- markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
- trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
- 0.0f
- },
- .max = {
- markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
- trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
- 0.0f
- },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t markerMaterial = {
- .unlit = {
- .color = COLOR_WHITE,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(
- spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial)
- );
- errorChain(spriteBatchFlush());
- }
- }
-
- char_t valueText[UI_SLIDER_VALUE_TEXT_MAX];
- if(slider->type == UI_SLIDER_TYPE_INT) {
- stringFormat(
- valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i
- );
- } else {
- stringFormat(
- valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f
- );
- }
-
- errorChain(textDraw(
- trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y,
- valueText, color, &FONT_DEFAULT
- ));
-
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uislider.h b/src/dusk/ui/widget/uislider.h
deleted file mode 100644
index d53dee00..00000000
--- a/src/dusk/ui/widget/uislider.h
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-#define UI_SLIDER_TRACK_WIDTH 80.0f
-#define UI_SLIDER_TRACK_HEIGHT 4.0f
-#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f
-#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f
-#define UI_SLIDER_GAP 4.0f
-#define UI_SLIDER_VALUE_TEXT_MAX 16
-
-/**
- * Number of discrete steps below which an int slider renders individual
- * step markers on its track instead of a smooth fill.
- */
-#define UI_SLIDER_STEP_MARKERS_MAX 10
-
-typedef enum {
- UI_SLIDER_TYPE_FLOAT,
- UI_SLIDER_TYPE_INT,
-
- UI_SLIDER_TYPE_COUNT
-} uislidertype_t;
-
-typedef union {
- float_t f;
- int32_t i;
-} uislidervalue_t;
-
-typedef struct {
- const char_t *label;
- uislidertype_t type;
- uislidervalue_t value;
- uislidervalue_t min;
- uislidervalue_t max;
- uislidervalue_t step;
- bool_t highlighted;
-} uislider_t;
-
-/**
- * Initializes a floating point slider.
- *
- * @param slider The slider to initialize.
- * @param label Display label.
- * @param value Initial value, clamped to [min, max].
- * @param min Minimum value.
- * @param max Maximum value.
- * @param step The amount a single step moves the value by.
- */
-void uiSliderInitFloat(
- uislider_t *slider,
- const char_t *label,
- const float_t value,
- const float_t min,
- const float_t max,
- const float_t step
-);
-
-/**
- * Initializes an integer slider.
- *
- * @param slider The slider to initialize.
- * @param label Display label.
- * @param value Initial value, clamped to [min, max].
- * @param min Minimum value.
- * @param max Maximum value.
- * @param step The amount a single step moves the value by.
- */
-void uiSliderInitInt(
- uislider_t *slider,
- const char_t *label,
- const int32_t value,
- const int32_t min,
- const int32_t max,
- const int32_t step
-);
-
-/**
- * Returns the slider's current value as a float. Works for both slider
- * types; int values are widened to float.
- *
- * @param slider The slider to query.
- * @returns The current value.
- */
-float_t uiSliderGetFloat(const uislider_t *slider);
-
-/**
- * Returns the slider's current value as an int. Only valid for
- * UI_SLIDER_TYPE_INT sliders.
- *
- * @param slider The slider to query.
- * @returns The current value.
- */
-int32_t uiSliderGetInt(const uislider_t *slider);
-
-/**
- * Sets the slider's value, clamped to [min, max]. Only valid for
- * UI_SLIDER_TYPE_FLOAT sliders.
- *
- * @param slider The slider to update.
- * @param value The new value.
- */
-void uiSliderSetFloat(uislider_t *slider, const float_t value);
-
-/**
- * Sets the slider's value, clamped to [min, max]. Only valid for
- * UI_SLIDER_TYPE_INT sliders.
- *
- * @param slider The slider to update.
- * @param value The new value.
- */
-void uiSliderSetInt(uislider_t *slider, const int32_t value);
-
-/**
- * Moves the slider's value up by one step, wrapping around to its min
- * if the step would exceed its max.
- *
- * @param slider The slider to update.
- */
-void uiSliderStepUp(uislider_t *slider);
-
-/**
- * Moves the slider's value down by one step, wrapping around to its
- * max if the step would go below its min.
- *
- * @param slider The slider to update.
- */
-void uiSliderStepDown(uislider_t *slider);
-
-/**
- * Returns the slider's current value normalized to a 0..1 range based
- * on its min/max.
- *
- * @param slider The slider to query.
- * @returns The normalized value.
- */
-float_t uiSliderGetRatio(const uislider_t *slider);
-
-/**
- * Returns the number of discrete steps between min and max. Always 0
- * for float sliders.
- *
- * @param slider The slider to query.
- * @returns The step count.
- */
-int32_t uiSliderGetStepCount(const uislider_t *slider);
-
-/**
- * Returns whether the slider is highlighted.
- *
- * @param slider The slider to query.
- * @returns True if highlighted.
- */
-bool_t uiSliderIsHighlighted(const uislider_t *slider);
-
-/**
- * Sets the highlighted state of the slider.
- *
- * @param slider The slider to update.
- * @param highlighted The new highlighted state.
- */
-void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted);
-
-/**
- * Draws the slider at the given screen position: label, then track,
- * then the current value as text. Int sliders with fewer than
- * UI_SLIDER_STEP_MARKERS_MAX discrete steps render a marker per step
- * instead of a plain track.
- *
- * @param slider The slider to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiSliderDraw(
- const uislider_t *slider,
- const float_t x,
- const float_t y
-);
diff --git a/src/dusk/ui/widget/uitab.c b/src/dusk/ui/widget/uitab.c
deleted file mode 100644
index 12073ef2..00000000
--- a/src/dusk/ui/widget/uitab.c
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "uitab.h"
-#include "assert/assert.h"
-#include "util/memory.h"
-#include "display/text/text.h"
-#include "display/color.h"
-#include "display/spritebatch/spritebatch.h"
-#include "display/texture/texture.h"
-#include "display/shader/shaderunlit.h"
-
-void uiTabInit(uitab_t *tab, const char_t *label) {
- assertNotNull(tab, "Tab cannot be NULL");
- assertNotNull(label, "Label cannot be NULL");
- memoryZero(tab, sizeof(uitab_t));
- tab->label = label;
-}
-
-bool_t uiTabIsActive(const uitab_t *tab) {
- assertNotNull(tab, "Tab cannot be NULL");
- return tab->active;
-}
-
-void uiTabSetActive(uitab_t *tab, const bool_t active) {
- assertNotNull(tab, "Tab cannot be NULL");
- tab->active = active;
-}
-
-errorret_t uiTabDraw(
- const uitab_t *tab,
- const float_t x,
- const float_t y
-) {
- assertNotNull(tab, "Tab cannot be NULL");
-
- int32_t labelW, labelH;
- textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH);
-
- spritebatchsprite_t sprite = {
- .min = { x, y, 0.0f },
- .max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f },
- .uvMin = { 0.0f, 0.0f },
- .uvMax = { 1.0f, 1.0f }
- };
- shadermaterial_t material = {
- .unlit = {
- .color = tab->active ? COLOR_GREEN : COLOR_RED,
- .texture = &TEXTURE_WHITE
- }
- };
- errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
- errorChain(spriteBatchFlush());
-
- errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT));
-
- errorOk();
-}
diff --git a/src/dusk/ui/widget/uitab.h b/src/dusk/ui/widget/uitab.h
deleted file mode 100644
index 678f54b6..00000000
--- a/src/dusk/ui/widget/uitab.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "error/error.h"
-
-typedef struct {
- const char_t *label;
- bool_t active;
-} uitab_t;
-
-/**
- * Initializes a tab.
- *
- * @param tab The tab to initialize.
- * @param label Display label.
- */
-void uiTabInit(uitab_t *tab, const char_t *label);
-
-/**
- * Returns whether the tab is active.
- *
- * @param tab The tab to query.
- * @returns True if active.
- */
-bool_t uiTabIsActive(const uitab_t *tab);
-
-/**
- * Sets the active state of the tab.
- *
- * @param tab The tab to update.
- * @param active The new active state.
- */
-void uiTabSetActive(uitab_t *tab, const bool_t active);
-
-/**
- * Draws the tab at the given screen position. Active tabs draw with a
- * green background; inactive tabs draw with a red background.
- *
- * @param tab The tab to draw.
- * @param x Screen x position.
- * @param y Screen y position.
- * @return Any error that occurs.
- */
-errorret_t uiTabDraw(
- const uitab_t *tab,
- const float_t x,
- const float_t y
-);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index c09ae827..d1f2d736 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -10,8 +10,6 @@ add_subdirectory(console)
add_subdirectory(error)
add_subdirectory(thread)
add_subdirectory(display)
-add_subdirectory(rpg)
-add_subdirectory(item)
add_subdirectory(save)
add_subdirectory(time)
add_subdirectory(util)
\ No newline at end of file
diff --git a/test/animation/CMakeLists.txt b/test/animation/CMakeLists.txt
index aabb6fdf..9f02402c 100644
--- a/test/animation/CMakeLists.txt
+++ b/test/animation/CMakeLists.txt
@@ -7,5 +7,3 @@ include(dusktest)
# Tests
dusktest(test_easing.c)
-dusktest(test_keyframe.c)
-dusktest(test_animation.c)
diff --git a/test/animation/test_animation.c b/test/animation/test_animation.c
deleted file mode 100644
index 39eb6b12..00000000
--- a/test/animation/test_animation.c
+++ /dev/null
@@ -1,443 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "animation/animation.h"
-#include "util/memory.h"
-
-typedef struct {
- uint16_t updateCount;
- uint16_t loopCount;
- uint16_t completeCount;
- float_t lastValues[8];
-} animationtestcallbacks_t;
-
-static void testOnUpdate(
- const uint16_t layer, const float_t value, void *user
-) {
- animationtestcallbacks_t *cb = (animationtestcallbacks_t *)user;
- cb->updateCount++;
- cb->lastValues[layer] = value;
-}
-
-static void testOnLoop(void *user) {
- ((animationtestcallbacks_t *)user)->loopCount++;
-}
-
-static void testOnComplete(void *user) {
- ((animationtestcallbacks_t *)user)->completeCount++;
-}
-
-static void animationTestInit(
- animation_t *anim,
- animationtestcallbacks_t *cb,
- keyframe_t *keyframes,
- uint16_t *keyframeCounts,
- const uint16_t layerCount
-) {
- memoryZero(cb, sizeof(animationtestcallbacks_t));
- animationInit(anim, keyframes, keyframeCounts, layerCount);
- anim->user = cb;
- anim->onUpdate = testOnUpdate;
- anim->onLoop = testOnLoop;
- anim->onComplete = testOnComplete;
-}
-
-static void test_animationInitSingleLayerDuration(void **state) {
- keyframe_t keyframes[3] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 3 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 1);
-
- assert_float_equal(anim.duration, 2.0f, 0.0001f);
- assert_int_equal(anim.layerCount, 1);
- assert_float_equal(anim.time, 0.0f, 0.0001f);
- assert_int_equal(anim.flags, 0);
-}
-
-static void test_animationInitMultiLayerDurationIsMax(void **state) {
- // Both layers must share the same keyframe count - animationInit/
- // animationGetLayerValue flatten the array as layer * keyframeCounts[layer],
- // which only produces the correct offset when every layer's count matches.
- keyframe_t keyframes[4] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
- { .time = 3.0f, .value = 300.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[2] = { 2, 2 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 2);
-
- // Layer 1's last keyframe (time=3) is later than layer 0's (time=1).
- assert_float_equal(anim.duration, 3.0f, 0.0001f);
-}
-
-static void test_animationInitNullAsserts(void **state) {
- keyframe_t keyframes[1] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 1 };
- animation_t anim;
-
- expect_assert_failure(animationInit(NULL, keyframes, keyframeCounts, 1));
- expect_assert_failure(animationInit(&anim, NULL, keyframeCounts, 1));
- expect_assert_failure(animationInit(&anim, keyframes, NULL, 1));
-}
-
-static void test_animationInitZeroLayerCountAsserts(void **state) {
- keyframe_t keyframes[1] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 1 };
- animation_t anim;
-
- expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 0));
-}
-
-static void test_animationInitUnsortedKeyframesAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
-
- expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
-}
-
-static void test_animationGetLayerValueSamplesEachLayerIndependently(
- void **state
-) {
- keyframe_t keyframes[4] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[2] = { 2, 2 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 2);
- anim.time = 1.0f;
-
- assert_float_equal(animationGetLayerValue(&anim, 0), 10.0f, 0.0001f);
- assert_float_equal(animationGetLayerValue(&anim, 1), 50.0f, 0.0001f);
-}
-
-static void test_animationGetLayerValueOutOfBoundsAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 1);
- expect_assert_failure(animationGetLayerValue(&anim, 1));
-}
-
-static void test_animationUpdateAdvancesTimeAndCallsOnUpdate(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- animationUpdate(&anim, 0.5f);
-
- assert_float_equal(anim.time, 0.5f, 0.0001f);
- assert_int_equal(cb.updateCount, 1);
- assert_float_equal(cb.lastValues[0], 5.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 0);
-}
-
-static void test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly(
- void **state
-) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
-
- // Overshoot the duration in a single update.
- animationUpdate(&anim, 5.0f);
- assert_float_equal(anim.time, 2.0f, 0.0001f);
- assert_float_equal(cb.lastValues[0], 20.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-
- // Further updates must not clamp past the end or refire onComplete.
- animationUpdate(&anim, 1.0f);
- assert_float_equal(anim.time, 2.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdateExactLandingOnEndFiresOnComplete(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
-
- // deltaTime exactly matches the remaining distance to the boundary.
- animationUpdate(&anim, 2.0f);
- assert_float_equal(anim.time, 2.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdateLoopWrapsAndFiresOnLoop(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_LOOP;
-
- // 2.5 durations worth of time: wraps twice, lands at 1.0 into the third.
- animationUpdate(&anim, 5.0f);
-
- assert_float_equal(anim.time, 1.0f, 0.0001f);
- assert_int_equal(cb.loopCount, 2);
- assert_int_equal(cb.completeCount, 0);
-}
-
-static void test_animationUpdateLoopStopEndCompletesInsteadOfWrapping(
- void **state
-) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_STOP_END;
-
- animationUpdate(&anim, 5.0f);
-
- assert_float_equal(anim.time, 2.0f, 0.0001f);
- assert_int_equal(cb.loopCount, 0);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdateReversePlaysBackward(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_REVERSE;
- anim.time = anim.duration;
-
- animationUpdate(&anim, 0.5f);
-
- assert_float_equal(anim.time, 1.5f, 0.0001f);
- assert_float_equal(cb.lastValues[0], 15.0f, 0.0001f);
-}
-
-static void test_animationUpdateReverseStopsAtBeginning(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_REVERSE;
- anim.time = anim.duration;
-
- animationUpdate(&anim, 10.0f);
-
- assert_float_equal(anim.time, 0.0f, 0.0001f);
- assert_float_equal(cb.lastValues[0], 0.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdatePingpongBouncesForeverWithoutStopFlags(
- void **state
-) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_PINGPONG;
-
- // 0 -> 2 (2s consumed), reflect, backward 1s more -> time=1, going backward.
- animationUpdate(&anim, 3.0f);
- assert_float_equal(anim.time, 1.0f, 0.0001f);
- assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0);
- assert_int_equal(cb.completeCount, 0);
-
- // Continue backward to 0 (1s), reflect, forward 1s more -> time=1, forward.
- animationUpdate(&anim, 2.0f);
- assert_float_equal(anim.time, 1.0f, 0.0001f);
- assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) == 0);
- assert_int_equal(cb.completeCount, 0);
-}
-
-static void test_animationUpdatePingpongStopEndCompletesAtEnd(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_END;
-
- animationUpdate(&anim, 10.0f);
-
- assert_float_equal(anim.time, 2.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdatePingpongStopBeginningCompletesAtStart(
- void **state
-) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_BEGINNING;
- anim.time = anim.duration;
- anim.flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
-
- animationUpdate(&anim, 10.0f);
-
- assert_float_equal(anim.time, 0.0f, 0.0001f);
- assert_int_equal(cb.completeCount, 1);
-}
-
-static void test_animationUpdateMultiLayerCallsOnUpdatePerLayer(void **state) {
- keyframe_t keyframes[4] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[2] = { 2, 2 };
- animation_t anim;
- animationtestcallbacks_t cb;
-
- animationTestInit(&anim, &cb, keyframes, keyframeCounts, 2);
- animationUpdate(&anim, 1.0f);
-
- assert_int_equal(cb.updateCount, 2);
- assert_float_equal(cb.lastValues[0], 10.0f, 0.0001f);
- assert_float_equal(cb.lastValues[1], 50.0f, 0.0001f);
-}
-
-static void test_animationUpdateNullAsserts(void **state) {
- expect_assert_failure(animationUpdate(NULL, 1.0f));
-}
-
-static void test_animationUpdateNegativeDeltaTimeAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 1);
- expect_assert_failure(animationUpdate(&anim, -1.0f));
-}
-
-static void test_animationInitZeroDurationAsserts(void **state) {
- keyframe_t keyframes[1] = {
- { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 1 };
- animation_t anim;
-
- expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
-}
-
-static void test_animationUpdateLoopAndPingpongTogetherAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
- uint16_t keyframeCounts[1] = { 2 };
- animation_t anim;
-
- animationInit(&anim, keyframes, keyframeCounts, 1);
- anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_PINGPONG;
- expect_assert_failure(animationUpdate(&anim, 0.1f));
-}
-
-int main(int argc, char **argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_animationInitSingleLayerDuration),
- cmocka_unit_test(test_animationInitMultiLayerDurationIsMax),
- cmocka_unit_test(test_animationInitNullAsserts),
- cmocka_unit_test(test_animationInitZeroLayerCountAsserts),
- cmocka_unit_test(test_animationInitUnsortedKeyframesAsserts),
- cmocka_unit_test(test_animationGetLayerValueSamplesEachLayerIndependently),
- cmocka_unit_test(test_animationGetLayerValueOutOfBoundsAsserts),
- cmocka_unit_test(test_animationUpdateAdvancesTimeAndCallsOnUpdate),
- cmocka_unit_test(test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly),
- cmocka_unit_test(test_animationUpdateExactLandingOnEndFiresOnComplete),
- cmocka_unit_test(test_animationUpdateLoopWrapsAndFiresOnLoop),
- cmocka_unit_test(test_animationUpdateLoopStopEndCompletesInsteadOfWrapping),
- cmocka_unit_test(test_animationUpdateReversePlaysBackward),
- cmocka_unit_test(test_animationUpdateReverseStopsAtBeginning),
- cmocka_unit_test(
- test_animationUpdatePingpongBouncesForeverWithoutStopFlags
- ),
- cmocka_unit_test(test_animationUpdatePingpongStopEndCompletesAtEnd),
- cmocka_unit_test(test_animationUpdatePingpongStopBeginningCompletesAtStart),
- cmocka_unit_test(test_animationUpdateMultiLayerCallsOnUpdatePerLayer),
- cmocka_unit_test(test_animationUpdateNullAsserts),
- cmocka_unit_test(test_animationUpdateNegativeDeltaTimeAsserts),
- cmocka_unit_test(test_animationInitZeroDurationAsserts),
- cmocka_unit_test(test_animationUpdateLoopAndPingpongTogetherAsserts),
- };
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/animation/test_keyframe.c b/test/animation/test_keyframe.c
deleted file mode 100644
index 5d6bf60d..00000000
--- a/test/animation/test_keyframe.c
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "animation/keyframe.h"
-
-static void test_keyframeGetValueSingleSegmentLinear(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.25f), 2.5f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.75f), 7.5f, 0.0001f);
-}
-
-static void test_keyframeGetValueMultiSegment(void **state) {
- keyframe_t keyframes[3] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
- };
-
- // Within the first segment.
- assert_float_equal(keyframeGetValue(keyframes, 3, 0.5f), 5.0f, 0.0001f);
-
- // Exactly on the interior keyframe, resolved as the end of segment one.
- assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f);
-
- // Within the second segment.
- assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 15.0f, 0.0001f);
-}
-
-static void test_keyframeGetValueDescendingValues(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 100.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 50.0f, 0.0001f);
-}
-
-static void test_keyframeGetValueAppliesEasing(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
-
- // EASING_IN_QUAD is t * t, so halfway through time should be a quarter of
- // the way through the value range, not half.
- assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f);
-}
-
-static void test_keyframeGetValueNonZeroStartTime(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 5.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 10.0f, .value = 100.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 0.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 7.5f), 50.0f, 0.0001f);
-}
-
-// NOTE: keyframeGetValue does not clamp to the first keyframe's value when
-// queried before the first keyframe's time - the start and end pointers
-// collapse onto the same keyframe, so the interpolation divides by a zero
-// time delta. This test documents that current behavior rather than
-// asserting it is desirable; flag to the maintainer if this should instead
-// clamp like the last-keyframe case does.
-static void test_keyframeGetValueBeforeFirstKeyframeIsNaN(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 3.0f, .value = 30.0f, .easing = EASING_LINEAR },
- };
-
- assert_true(isnan(keyframeGetValue(keyframes, 2, 0.0f)));
-}
-
-static void test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue(
- void **state
-) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 2, 2.0f), 10.0f, 0.0001f);
-}
-
-static void test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue(
- void **state
-) {
- keyframe_t keyframes[1] = {
- { .time = 5.0f, .value = 42.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 1, 5.0f), 42.0f, 0.0001f);
- assert_float_equal(keyframeGetValue(keyframes, 1, 10.0f), 42.0f, 0.0001f);
-
- // Before the single keyframe's time is still the documented NaN case.
- assert_true(isnan(keyframeGetValue(keyframes, 1, 0.0f)));
-}
-
-static void test_keyframeGetValueNullKeyframesAsserts(void **state) {
- expect_assert_failure(keyframeGetValue(NULL, 1, 0.0f));
-}
-
-static void test_keyframeGetValueZeroCountAsserts(void **state) {
- keyframe_t keyframes[1] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
-
- expect_assert_failure(keyframeGetValue(keyframes, 0, 0.0f));
-}
-
-static void test_keyframeGetValueNegativeTimeAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
-
- expect_assert_failure(keyframeGetValue(keyframes, 2, -1.0f));
-}
-
-static void test_keyframeGetValueUnsortedKeyframesAsserts(void **state) {
- keyframe_t keyframes[2] = {
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- };
-
- expect_assert_failure(keyframeGetValue(keyframes, 2, 0.0f));
-}
-
-static void test_keyframeGetValueLaterKeyframeOutOfOrderAsserts(void **state) {
- // The first pair is sorted, but the third keyframe is out of order - the
- // check must walk the whole array, not just the first pair.
- keyframe_t keyframes[3] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- { .time = 0.5f, .value = 5.0f, .easing = EASING_LINEAR },
- };
-
- expect_assert_failure(keyframeGetValue(keyframes, 3, 0.0f));
-}
-
-static void test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert(
- void **state
-) {
- // Equal (non-decreasing) times are allowed - only strictly decreasing
- // times should trigger the sorted check.
- keyframe_t keyframes[3] = {
- { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
- { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
- { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
- };
-
- assert_float_equal(keyframeGetValue(keyframes, 3, 0.0f), 5.0f, 0.0001f);
-}
-
-int main(int argc, char **argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_keyframeGetValueSingleSegmentLinear),
- cmocka_unit_test(test_keyframeGetValueMultiSegment),
- cmocka_unit_test(test_keyframeGetValueDescendingValues),
- cmocka_unit_test(test_keyframeGetValueAppliesEasing),
- cmocka_unit_test(test_keyframeGetValueNonZeroStartTime),
- cmocka_unit_test(test_keyframeGetValueBeforeFirstKeyframeIsNaN),
- cmocka_unit_test(
- test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue
- ),
- cmocka_unit_test(test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue),
- cmocka_unit_test(test_keyframeGetValueNullKeyframesAsserts),
- cmocka_unit_test(test_keyframeGetValueZeroCountAsserts),
- cmocka_unit_test(test_keyframeGetValueNegativeTimeAsserts),
- cmocka_unit_test(test_keyframeGetValueUnsortedKeyframesAsserts),
- cmocka_unit_test(test_keyframeGetValueLaterKeyframeOutOfOrderAsserts),
- cmocka_unit_test(test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert),
- };
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/item/CMakeLists.txt b/test/item/CMakeLists.txt
deleted file mode 100644
index ee723335..00000000
--- a/test/item/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-include(dusktest)
-
-# Tests
-dusktest(test_inventory.c)
\ No newline at end of file
diff --git a/test/item/test_inventory.c b/test/item/test_inventory.c
deleted file mode 100644
index 75d13302..00000000
--- a/test/item/test_inventory.c
+++ /dev/null
@@ -1,433 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/item/inventory.h"
-#include "util/memory.h"
-
-static itemid_t ITEM_ID_POTION;
-static itemid_t ITEM_ID_POTATO;
-static itemid_t ITEM_ID_APPLE;
-
-// Registers three test items (POTION as MEDICINE, POTATO/APPLE as FOOD)
-// directly into the global ITEM registry -- these tests exercise the
-// inventory/backpack layer only, not the JSON-driven itemInit() pipeline.
-static int_t itemFixtureSetup(void **state) {
- memoryZero(&ITEM, sizeof(ITEM));
- ITEM.typeCount = ITEM_TYPE_NULL + 1;
- ITEM.defCount = ITEM_ID_NULL + 1;
-
- const itemtype_t medicine = itemTypeGetOrCreate("MEDICINE");
- const itemtype_t food = itemTypeGetOrCreate("FOOD");
-
- ITEM_ID_POTION = ITEM.defCount++;
- ITEM.defs[ITEM_ID_POTION].type = medicine;
-
- ITEM_ID_POTATO = ITEM.defCount++;
- ITEM.defs[ITEM_ID_POTATO].type = food;
-
- ITEM_ID_APPLE = ITEM.defCount++;
- ITEM.defs[ITEM_ID_APPLE].type = food;
-
- return 0;
-}
-
-static void test_inventoryInit(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
-
- inventoryInit(&inventory, storage, 5);
-
- // Should setup struct
- assert_non_null(inventory.storage);
- assert_int_equal(inventory.storageSize, 5);
-
- // Should zero the item ids.
- for(size_t i = 0; i < inventory.storageSize; i++) {
- assert_int_equal(inventory.storage[i].item, ITEM_ID_NULL);
- assert_int_equal(inventory.storage[i].quantity, 0);
- }
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryInit(NULL, storage, 5));
-
- // Should fail when given NULL storage pointer
- expect_assert_failure(inventoryInit(&inventory, NULL, 5));
-
- // Should fail when given zero storage size
- expect_assert_failure(inventoryInit(&inventory, storage, 0));
-}
-
-static void test_inventoryItemExists(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Initially should not exist
- assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
-
- // Add item
- inventorySet(&inventory, ITEM_ID_POTION, 3);
- assert_true(inventoryItemExists(&inventory, ITEM_ID_POTION));
-
- // Remove item
- inventorySet(&inventory, ITEM_ID_POTION, 0);
- assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryItemExists(NULL, ITEM_ID_POTION));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
- inventory.storageSize = 5;
-
- // Should fail when given ITEM_ID_NULL
- expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_NULL));
-
- // Should fail if item has zero quantity somehow
- inventorySet(&inventory, ITEM_ID_POTION, 3);
- inventory.storage[0].quantity = 0;
- expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
-}
-
-static void test_inventorySet(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Set item quantity
- inventorySet(&inventory, ITEM_ID_POTION, 4);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].quantity, 4);
-
- // Update item quantity
- inventorySet(&inventory, ITEM_ID_POTION, 2);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].quantity, 2);
-
- // Remove item by setting quantity to 0
- inventorySet(&inventory, ITEM_ID_POTION, 0);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
-
- // Setting multiple items should not cause issues
- inventorySet(&inventory, ITEM_ID_POTION, 1);
- inventorySet(&inventory, ITEM_ID_POTATO, 5);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].quantity, 1);
- assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
- assert_int_equal(inventory.storage[1].quantity, 5);
- assert_int_equal(inventory.storage[2].item, ITEM_ID_NULL);
-
- // Setting early item to 0 should shift others down
- inventorySet(&inventory, ITEM_ID_POTION, 0);// Item 0 removed
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTATO);
- assert_int_equal(inventory.storage[0].quantity, 5);
- assert_int_equal(inventory.storage[1].item, ITEM_ID_NULL);
-
- // Setting non-existing item should add it
- inventorySet(&inventory, ITEM_ID_POTION, 3);
- assert_int_equal(inventory.storage[1].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[1].quantity, 3);
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventorySet(NULL, ITEM_ID_POTION, 3));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
- inventory.storageSize = 5;
-
- // Should fail when given ITEM_ID_NULL
- expect_assert_failure(inventorySet(&inventory, ITEM_ID_NULL, 3));
-}
-
-static void test_inventoryAdd(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Add item quantity
- inventoryAdd(&inventory, ITEM_ID_POTION, 4);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].quantity, 4);
-
- // Add more to existing item
- inventoryAdd(&inventory, ITEM_ID_POTION, 3);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].quantity, 7);
-
- // Adding item that would overflow should assert
- expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 250));
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryAdd(NULL, ITEM_ID_POTION, 3));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
- inventory.storageSize = 5;
-
- // Should fail when given ITEM_ID_NULL
- expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_NULL, 3));
-}
-
-static void test_inventoryRemove(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Add item and then remove it
- inventorySet(&inventory, ITEM_ID_POTION, 5);
- inventoryRemove(&inventory, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
-
- // Removing non-existing item should do nothing
- inventoryRemove(&inventory, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryRemove(NULL, ITEM_ID_POTION));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
- inventory.storageSize = 5;
-
- // Should fail when given ITEM_ID_NULL
- expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_NULL));
-}
-
-static void test_inventoryGetCount(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Initially should be zero
- assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
-
- // Add item and check count
- inventorySet(&inventory, ITEM_ID_POTION, 4);
- assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 4);
-
- // Update item quantity and check count
- inventorySet(&inventory, ITEM_ID_POTION, 2);
- assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 2);
-
- // Remove item and check count
- inventorySet(&inventory, ITEM_ID_POTION, 0);
- assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryGetCount(NULL, ITEM_ID_POTION));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
- inventory.storageSize = 5;
-
- // Should fail when given ITEM_ID_NULL
- expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_NULL));
-}
-
-static void test_inventoryIsFull(void **state) {
-
-
- inventorystack_t storage[2];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 2);
-
- // Initially should not be full
- assert_false(inventoryIsFull(&inventory));
-
- // Fill inventory
- inventorySet(&inventory, ITEM_ID_POTION, 1);
- inventorySet(&inventory, ITEM_ID_POTATO, 1);
- assert_true(inventoryIsFull(&inventory));
-
- // Remove one item
- inventoryRemove(&inventory, ITEM_ID_POTION);
- assert_false(inventoryIsFull(&inventory));
-
- // Add one item back
- inventorySet(&inventory, ITEM_ID_POTION, 1);
- assert_true(inventoryIsFull(&inventory));
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryIsFull(NULL));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryIsFull(&inventory));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryIsFull(&inventory));
- inventory.storageSize = 2;
-}
-
-static void test_inventoryItemFull(void **state) {
-
-
- inventorystack_t storage[2];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 2);
-
- // Initially should not be full
- assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
-
- // Add some potions, but not too many.
- inventorySet(&inventory, ITEM_ID_POTION, 100);
- assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
-
- // Fill with potions
- inventorySet(&inventory, ITEM_ID_POTION, ITEM_STACK_QUANTITY_MAX);
- assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
-
- // Add potatoes, should not affect potions
- inventorySet(&inventory, ITEM_ID_POTATO, 50);
- assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
- assert_false(inventoryItemFull(&inventory, ITEM_ID_POTATO));
-
- // Remove some potions
- inventorySet(&inventory, ITEM_ID_POTION, 200);
- assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
-
- // Fill with potatoes
- inventorySet(&inventory, ITEM_ID_POTATO, ITEM_STACK_QUANTITY_MAX);
- assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
- assert_true(inventoryItemFull(&inventory, ITEM_ID_POTATO));
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventoryItemFull(NULL, ITEM_ID_POTION));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
- inventory.storageSize = 2;
-}
-
-static void test_inventorySort(void **state) {
-
-
- inventorystack_t storage[5];
- inventory_t inventory;
- inventoryInit(&inventory, storage, 5);
-
- // Add items out of order
- inventorySet(&inventory, ITEM_ID_POTATO, 3);
- inventorySet(&inventory, ITEM_ID_APPLE, 5);
- inventorySet(&inventory, ITEM_ID_POTION, 2);
-
- // Sort by ID
- inventorySort(&inventory, INVENTORY_SORT_BY_ID, false);
-
- // Check order
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
- assert_int_equal(inventory.storage[2].item, ITEM_ID_APPLE);
-
- // Sort by ID reverse
- inventorySort(&inventory, INVENTORY_SORT_BY_ID, true);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_APPLE);
- assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
- assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
-
- // Sort by type ascending - POTION (MEDICINE) sorts before POTATO/APPLE
- // (both FOOD). The two FOOD items compare equal, so their relative order
- // isn't guaranteed (qsort is not stable) - only the type grouping is.
- inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, false);
- assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
- assert_true(
- (inventory.storage[1].item == ITEM_ID_POTATO &&
- inventory.storage[2].item == ITEM_ID_APPLE) ||
- (inventory.storage[1].item == ITEM_ID_APPLE &&
- inventory.storage[2].item == ITEM_ID_POTATO)
- );
-
- // Sort by type reverse - FOOD items first (order unspecified), POTION last.
- inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, true);
- assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
- assert_true(
- (inventory.storage[0].item == ITEM_ID_POTATO &&
- inventory.storage[1].item == ITEM_ID_APPLE) ||
- (inventory.storage[0].item == ITEM_ID_APPLE &&
- inventory.storage[1].item == ITEM_ID_POTATO)
- );
-
- // Should fail when given NULL inventory pointer
- expect_assert_failure(inventorySort(NULL, INVENTORY_SORT_BY_ID, false));
-
- // Should fail when given NULL storage pointer
- inventory.storage = NULL;
- expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
- inventory.storage = storage;
-
- // Should fail when given zero storage size
- inventory.storageSize = 0;
- expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
- inventory.storageSize = 5;
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_inventoryInit),
- cmocka_unit_test(test_inventoryItemExists),
- cmocka_unit_test(test_inventorySet),
- cmocka_unit_test(test_inventoryAdd),
- cmocka_unit_test(test_inventoryRemove),
- cmocka_unit_test(test_inventoryGetCount),
- cmocka_unit_test(test_inventoryIsFull),
- cmocka_unit_test(test_inventoryItemFull),
- cmocka_unit_test(test_inventorySort),
- };
-
- return cmocka_run_group_tests(tests, itemFixtureSetup, NULL);
-}
\ No newline at end of file
diff --git a/test/rpg/CMakeLists.txt b/test/rpg/CMakeLists.txt
deleted file mode 100644
index bd2d8fb2..00000000
--- a/test/rpg/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
-
-include(dusktest)
-
-# Tests
-dusktest(test_rpg.c)
-
-# Subdirs
-add_subdirectory(overworld)
-add_subdirectory(battle)
-add_subdirectory(entity)
-add_subdirectory(cutscene)
\ No newline at end of file
diff --git a/test/rpg/battle/CMakeLists.txt b/test/rpg/battle/CMakeLists.txt
deleted file mode 100644
index b9d92cfd..00000000
--- a/test/rpg/battle/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-include(dusktest)
-
-# Tests
-dusktest(test_battle.c)
diff --git a/test/rpg/battle/test_battle.c b/test/rpg/battle/test_battle.c
deleted file mode 100644
index c2112ba1..00000000
--- a/test/rpg/battle/test_battle.c
+++ /dev/null
@@ -1,271 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/battle/battle.h"
-
-// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup.
-#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1)
-
-static battlefighter_t *addFighter(
- const battlefighterteam_t team,
- const battlefightercontroller_t controller,
- const uint16_t attack,
- const uint16_t defense,
- const uint16_t speed,
- const uint16_t healthMax
-) {
- const battlefighterstats_t stats = {
- .attack = attack, .defense = defense, .speed = speed
- };
- return battleAddFighter(team, controller, stats, healthMax, 0);
-}
-
-static void test_battleStartEntersOpeningThenPreRound(void **state) {
-
-
- battleInit();
- addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 10, 0, 10, 20
- );
- addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 10, 0, 5, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- assert_int_equal(BATTLE.state, BATTLE_STATE_OPENING);
-
- // OPENING only transitions to PRE_ROUND; it doesn't run PRE_ROUND's logic
- // in the same call.
- battleUpdate();
- assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
- assert_int_equal(BATTLE.executionCount, 0);
-
- // PRE_ROUND builds the execution order and moves on to selection.
- battleUpdate();
- assert_int_equal(BATTLE.executionCount, 2);
- assert_true(
- BATTLE.state == BATTLE_STATE_PLAYER_SELECTION ||
- BATTLE.state == BATTLE_STATE_AI_SELECTION
- );
-}
-
-static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters(
- void **state
-) {
-
-
- battleInit();
- battlefighter_t *ally1 = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 100, 20
- );
- battlefighter_t *ally2 = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
- );
- battlefighter_t *enemy = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 50, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION, positions the cursor
-
- // ally1 is fastest, so it's first to decide.
- assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
- assert_ptr_equal(battleGetCurrentFighter(), ally1);
-
- // A forced move (as a scripted-battle cutscene item would push) marks
- // ally2 as already decided, so PLAYER_SELECTION must skip it.
- battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id);
-
- battlePlayerAttack(enemy->id);
- assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK);
- assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id);
-
- // ally2 already had a decision queued, and enemy is AI-controlled, so
- // there's nothing left for PLAYER_SELECTION -- it should have moved on.
- assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
-}
-
-static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) {
-
-
- battleInit();
- battlefighter_t *ally = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
- );
- battlefighter_t *enemy = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
-
- battlePlayerAttack(enemy->id);// ally decides, selection moves to AI
- assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
-
- battleUpdate();// AI_SELECTION auto-decides for enemy
- assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
- assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK);
- assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id);
-}
-
-static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles(
- void **state
-) {
-
-
- battleInit();
- // ally1 is fast enough to kill enemy1 before enemy1 or ally2 act. enemy2
- // keeps the battle alive so execution continues to ally2's now-moot
- // attack against the already-dead enemy1.
- battlefighter_t *ally1 = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 100, 20
- );
- battlefighter_t *ally2 = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
- );
- battlefighter_t *enemy1 = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 50, 10
- );
- battlefighter_t *enemy2 = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 1, 100
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION (ally1 first, fastest)
-
- assert_ptr_equal(battleGetCurrentFighter(), ally1);
- battlePlayerAttack(enemy1->id);
- assert_ptr_equal(battleGetCurrentFighter(), ally2);
- battlePlayerAttack(enemy1->id);// both allies target enemy1
- assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
-
- battleUpdate();// AI_SELECTION
- assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
-
- // Execution order: ally1 (100), enemy1 (50), ally2 (10), enemy2 (1).
- battleUpdate();// ally1 kills enemy1
- assert_false(battleFighterIsAlive(enemy1));
- assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);// enemy2 alive
-
- battleUpdate();// enemy1's own (now dead) turn is skipped
- battleUpdate();// ally2's attack against the dead enemy1 fizzles silently
- assert_int_equal(enemy1->health, 0);// no double-kill, no underflow
-
- battleUpdate();// enemy2 acts
- battleUpdate();// all 4 slots processed -> POST_ROUND
- assert_int_equal(BATTLE.state, BATTLE_STATE_POST_ROUND);
-
- battleUpdate();// POST_ROUND -> PRE_ROUND, round advances
- assert_int_equal(BATTLE.round, 2);
- assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
-}
-
-static void test_battleWinEndsInStateEnded(void **state) {
-
-
- battleInit();
- battlefighter_t *ally = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
- );
- battlefighter_t *enemy = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 10
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
-
- battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
- battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
- battleUpdate();// ally kills the only enemy
-
- assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
- assert_int_equal(BATTLE.result, BATTLE_RESULT_WIN);
-}
-
-static void test_battleLossEndsInStateEnded(void **state) {
-
-
- battleInit();
- battlefighter_t *ally = addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 5, 10
- );
- battlefighter_t *enemy = addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 50, 0, 10, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
-
- battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
- battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
- battleUpdate();// ally attacks first (irrelevant to outcome)
- battleUpdate();// enemy kills the only ally
-
- assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
- assert_int_equal(BATTLE.result, BATTLE_RESULT_LOSS);
-}
-
-static void test_battleFleeEndsInStateEndedImmediately(void **state) {
-
-
- battleInit();
- addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
- );
- addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, true);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
-
- battlePlayerFlee();
- assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
- assert_int_equal(BATTLE.result, BATTLE_RESULT_FLED);
-}
-
-static void test_battleFleeUnavailableIsIgnored(void **state) {
-
-
- battleInit();
- addFighter(
- BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
- );
- addFighter(
- BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
- );
-
- battleStart(BATTLE_ENCOUNTER_REGULAR, false);
- battleUpdate();// OPENING -> PRE_ROUND
- battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
-
- battlePlayerFlee();
- assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
- assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE);
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_battleStartEntersOpeningThenPreRound),
- cmocka_unit_test(test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters),
- cmocka_unit_test(test_battleAiSelectionAutoQueuesUndecidedAiFighters),
- cmocka_unit_test(test_battleMovesExecutingResolvesInSpeedOrderAndFizzles),
- cmocka_unit_test(test_battleWinEndsInStateEnded),
- cmocka_unit_test(test_battleLossEndsInStateEnded),
- cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately),
- cmocka_unit_test(test_battleFleeUnavailableIsIgnored),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/cutscene/CMakeLists.txt b/test/rpg/cutscene/CMakeLists.txt
deleted file mode 100644
index 52464928..00000000
--- a/test/rpg/cutscene/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-include(dusktest)
-
-# Tests
-dusktest(test_cutscene.c)
-dusktest(test_cutscenesystem.c)
-dusktest(test_cutscenecontrol.c)
-dusktest(test_cutscenemaparea.c)
diff --git a/test/rpg/cutscene/test_cutscene.c b/test/rpg/cutscene/test_cutscene.c
deleted file mode 100644
index 2f0457cf..00000000
--- a/test/rpg/cutscene/test_cutscene.c
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/cutscene/cutscene.h"
-#include "util/memory.h"
-#include "util/string.h"
-
-// ============================================================
-// cutsceneRemoveFront
-// ============================================================
-
-static cutsceneitem_t MARKER_ITEM(const char_t *name) {
- cutsceneitem_t item;
- memoryZero(&item, sizeof(item));
- item.type = CUTSCENE_ITEM_TYPE_MARKER;
- stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1);
- return item;
-}
-
-static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero(
- void **state
-) {
-
- cutsceneitem_t items[4] = {
- MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d")
- };
- cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 };
-
- cutsceneRemoveFront(&scene, 2);
-
- assert_int_equal(scene.itemCount, 2);
- assert_string_equal(scene.items[0].marker.name, "c");
- assert_string_equal(scene.items[1].marker.name, "d");
-}
-
-static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) {
-
- cutsceneitem_t items[3] = {
- MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c")
- };
- cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 };
-
- cutsceneRemoveFront(&scene, 3);
-
- assert_int_equal(scene.itemCount, 0);
-}
-
-static void test_cutsceneRemoveFrontZeroIsNoop(void **state) {
-
- cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
- cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
-
- cutsceneRemoveFront(&scene, 0);
-
- assert_int_equal(scene.itemCount, 2);
- assert_string_equal(scene.items[0].marker.name, "a");
- assert_string_equal(scene.items[1].marker.name, "b");
-}
-
-static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount(
- void **state
-) {
-
- cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
- cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
-
- expect_assert_failure(cutsceneRemoveFront(&scene, 3));
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero),
- cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene),
- cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop),
- cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/cutscene/test_cutscenecontrol.c b/test/rpg/cutscene/test_cutscenecontrol.c
deleted file mode 100644
index 60b4d57d..00000000
--- a/test/rpg/cutscene/test_cutscenecontrol.c
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "time/time.h"
-#include "util/memory.h"
-
-static void test_cutsceneWaitCompletesAfterItsDuration(void **state) {
-
- cutsceneitem_t item = { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
- cutsceneitemdata_t data;
- memoryZero(&data, sizeof(data));
-
- cutsceneWaitStart(&item, &data);
-
- TIME.delta = 0.5f;
- assert_false(cutsceneWaitUpdate(&item, &data));
-
- TIME.delta = 0.6f;
- assert_true(cutsceneWaitUpdate(&item, &data));
-}
-
-static void *lastUserData;
-static uint8_t callbackCallCount;
-
-static void recordCallback(void *userData) {
- lastUserData = userData;
- callbackCallCount++;
-}
-
-static void test_cutsceneCallbackFiresOnStartWithNullUserData(void **state) {
-
- callbackCallCount = 0;
- lastUserData = (void *)0x1;
-
- cutsceneitem_t item =
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback };
- cutsceneitemdata_t data;
-
- cutsceneCallbackStart(&item, &data);
- assert_int_equal(callbackCallCount, 1);
- assert_null(lastUserData);
-
- // Callback items always complete immediately -- the effect already
- // happened in Start, not Update.
- assert_true(cutsceneCallbackUpdate(&item, &data));
- assert_int_equal(callbackCallCount, 1);// Update doesn't fire it again
-}
-
-static void test_cutsceneCallbackNullIsNoop(void **state) {
-
- callbackCallCount = 0;
- cutsceneitem_t item =
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = NULL };
- cutsceneitemdata_t data;
-
- cutsceneCallbackStart(&item, &data);// should not crash
- assert_int_equal(callbackCallCount, 0);
-}
-
-static void test_cutsceneSetPauseAppliesImmediately(void **state) {
-
- CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_SET_PAUSE,
- .setPause = CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
- };
- cutsceneitemdata_t data;
-
- cutsceneSetPauseStart(&item, &data);
- assert_int_equal(
- CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
- );
- assert_true(cutsceneSetPauseUpdate(&item, &data));
-}
-
-static void test_cutsceneConcurrentCompletesOnceAllChildrenDo(void **state) {
-
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_CONCURRENT,
- .concurrent = {
- .items = (const cutsceneitem_t[]){
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.2f },
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }
- },
- .count = 2
- }
- };
- cutsceneitemdata_t data;
- memoryZero(&data, sizeof(data));
-
- cutsceneConcurrentStart(&item, &data);
-
- TIME.delta = 0.3f;
- // Child 0 (0.2s) elapses; child 1 (0.5s) still has 0.2s left.
- assert_false(cutsceneConcurrentUpdate(&item, &data));
-
- TIME.delta = 0.3f;
- // Child 1 now elapses too.
- assert_true(cutsceneConcurrentUpdate(&item, &data));
-}
-
-static void test_cutsceneConcurrentDoesNotReUpdateFinishedChildren(
- void **state
-) {
-
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_CONCURRENT,
- .concurrent = {
- .items = (const cutsceneitem_t[]){
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.1f },
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 10.0f }
- },
- .count = 2
- }
- };
- cutsceneitemdata_t data;
- memoryZero(&data, sizeof(data));
-
- cutsceneConcurrentStart(&item, &data);
-
- TIME.delta = 0.2f;
- cutsceneConcurrentUpdate(&item, &data);// child 0 finishes
-
- // If child 0 were re-updated, its stored wait value would keep dropping
- // further below zero -- not observable directly, but the overall result
- // must not falsely report done just because child 0 keeps completing.
- TIME.delta = 0.2f;
- assert_false(cutsceneConcurrentUpdate(&item, &data));
-}
-
-static void test_cutsceneConcurrentCannotNest(void **state) {
-
- cutsceneitem_t outer = {
- .type = CUTSCENE_ITEM_TYPE_CONCURRENT,
- .concurrent = {
- .items = (const cutsceneitem_t[]){
- {
- .type = CUTSCENE_ITEM_TYPE_CONCURRENT,
- .concurrent = {
- .items = (const cutsceneitem_t[]){
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
- },
- .count = 1
- }
- }
- },
- .count = 1
- }
- };
- cutsceneitemdata_t data;
-
- expect_assert_failure(cutsceneConcurrentStart(&outer, &data));
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_cutsceneWaitCompletesAfterItsDuration),
- cmocka_unit_test(test_cutsceneCallbackFiresOnStartWithNullUserData),
- cmocka_unit_test(test_cutsceneCallbackNullIsNoop),
- cmocka_unit_test(test_cutsceneSetPauseAppliesImmediately),
- cmocka_unit_test(test_cutsceneConcurrentCompletesOnceAllChildrenDo),
- cmocka_unit_test(test_cutsceneConcurrentDoesNotReUpdateFinishedChildren),
- cmocka_unit_test(test_cutsceneConcurrentCannotNest),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/cutscene/test_cutscenemaparea.c b/test/rpg/cutscene/test_cutscenemaparea.c
deleted file mode 100644
index 9c70980f..00000000
--- a/test/rpg/cutscene/test_cutscenemaparea.c
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/overworld/maparea.h"
-#include "util/memory.h"
-
-static void resetMapAreas(void) {
- memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
-}
-
-static void test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated(
- void **state
-) {
-
- resetMapAreas();
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
- .mapAreaAdd = {
- .min = { 0, 0, 0 }, .max = { 5, 5, 0 },
- .callback = mapAreaNoopCallback,
- .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
- }
- };
- cutsceneitemdata_t data;
-
- cutsceneMapAreaAddStart(&item, &data);
- assert_true(cutsceneMapAreaAddUpdate(&item, &data));
-
- uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
- assert_ptr_equal(MAP_AREAS[id].callback, mapAreaNoopCallback);
- assert_int_equal(MAP_AREAS[id].max.x, 5);
-}
-
-static void test_cutsceneMapAreaRemoveClearsSlot(void **state) {
-
- resetMapAreas();
- uint8_t id = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
- .mapAreaRemove = { .areaId = id }
- };
- cutsceneitemdata_t data;
-
- cutsceneMapAreaRemoveStart(&item, &data);
- assert_true(cutsceneMapAreaRemoveUpdate(&item, &data));
- assert_null(MAP_AREAS[id].callback);
-}
-
-static void test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel(
- void **state
-) {
-
- resetMapAreas();
- cutsceneitem_t addItem = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
- .mapAreaAdd = {
- .min = { 0, 0, 0 }, .max = { 5, 5, 0 },
- .callback = mapAreaNoopCallback,
- .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
- }
- };
- cutsceneitemdata_t data;
- cutsceneMapAreaAddStart(&addItem, &data);
- uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
-
- cutsceneitem_t removeItem = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
- .mapAreaRemove = { .areaId = CUTSCENE_REF_AREA_LAST_CREATED }
- };
- cutsceneMapAreaRemoveStart(&removeItem, &data);
- assert_null(MAP_AREAS[id].callback);
-}
-
-static void test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges(
- void **state
-) {
-
- resetMapAreas();
- uint8_t id = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
- .mapAreaWait = { .areaIds = { id }, .count = 1 }
- };
- cutsceneitemdata_t data;
-
- cutsceneMapAreaWaitStart(&item, &data);
- assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
-
- MAP_AREAS[id].triggerCount++;// simulates the area's callback firing
- assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
-}
-
-static void test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges(
- void **state
-) {
-
- resetMapAreas();
- uint8_t idA = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
- uint8_t idB = mapAreaAdd(
- (worldpos_t){ 10, 10, 0 }, (worldpos_t){ 15, 15, 0 },
- mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- cutsceneitem_t item = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
- .mapAreaWait = { .areaIds = { idA, idB }, .count = 2 }
- };
- cutsceneitemdata_t data;
-
- cutsceneMapAreaWaitStart(&item, &data);
- assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
-
- MAP_AREAS[idB].triggerCount++;// only the second watched area fires
- assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
-}
-
-static void test_cutsceneMapAreaWaitResolvesLastCreatedSentinel(
- void **state
-) {
-
- resetMapAreas();
- cutsceneitem_t addItem = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
- .mapAreaAdd = {
- .min = { 0, 0, 0 }, .max = { 5, 5, 0 },
- .callback = mapAreaNoopCallback,
- .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
- }
- };
- cutsceneitemdata_t data;
- cutsceneMapAreaAddStart(&addItem, &data);
- uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
-
- cutsceneitem_t waitItem = {
- .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
- .mapAreaWait = { .areaIds = { CUTSCENE_REF_AREA_LAST_CREATED }, .count = 1 }
- };
- cutsceneMapAreaWaitStart(&waitItem, &data);
- assert_false(cutsceneMapAreaWaitUpdate(&waitItem, &data));
-
- MAP_AREAS[id].triggerCount++;
- assert_true(cutsceneMapAreaWaitUpdate(&waitItem, &data));
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated),
- cmocka_unit_test(test_cutsceneMapAreaRemoveClearsSlot),
- cmocka_unit_test(test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel),
- cmocka_unit_test(test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges),
- cmocka_unit_test(
- test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges
- ),
- cmocka_unit_test(test_cutsceneMapAreaWaitResolvesLastCreatedSentinel),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/cutscene/test_cutscenesystem.c b/test/rpg/cutscene/test_cutscenesystem.c
deleted file mode 100644
index 845257fd..00000000
--- a/test/rpg/cutscene/test_cutscenesystem.c
+++ /dev/null
@@ -1,583 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "rpg/entity/entity.h"
-#include "time/time.h"
-#include "util/string.h"
-
-static uint8_t callbackFired;
-
-static void recordCallback(void *userData) {
- callbackFired++;
-}
-
-// INNER is jumped into by OUTER's second item, never returning -- this is
-// the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart
-// replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect
-// fires in Start, not Update, so it fires the same frame the jump happens.
-static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback }
-};
-static cutscene_t CUTSCENE_TEST_INNER = {
- .items = CUTSCENE_TEST_INNER_ITEMS,
- .itemCount = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f },
- {
- .type = CUTSCENE_ITEM_TYPE_CUTSCENE,
- .cutsceneRef = { .cutscene = &CUTSCENE_TEST_INNER }
- }
-};
-static cutscene_t CUTSCENE_TEST_OUTER = {
- .items = CUTSCENE_TEST_OUTER_ITEMS,
- .itemCount = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_DEFAULT
-};
-
-static void test_cutsceneSystemStartSetsUpInitialItem(void **state) {
-
- cutsceneSystemInit();
-
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
- assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
- assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_DEFAULT);
-}
-
-static void test_cutsceneSystemNestedCutsceneIsOneWayJump(void **state) {
-
- cutsceneSystemInit();
- callbackFired = 0;
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
-
- TIME.delta = 0.1f;
- cutsceneSystemUpdate();// wait not elapsed yet
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
- assert_int_equal(callbackFired, 0);
-
- // The wait elapses, advancing to the nested-cutscene item, which jumps
- // straight into INNER and starts its first item (the callback) -- all
- // within this single update call.
- TIME.delta = 1.0f;
- cutsceneSystemUpdate();
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INNER);
- assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
- assert_int_equal(callbackFired, 1);
-
- // INNER's only item (the callback) always reports complete -- one more
- // update ends the whole cutscene.
- cutsceneSystemUpdate();
- assert_null(CUTSCENE_SYSTEM.scene);
- assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
- assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
-}
-
-static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) {
-
- cutsceneSystemInit();
-
- cutsceneSystemUpdate();// should not crash
- assert_null(CUTSCENE_SYSTEM.scene);
-}
-
-static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
-};
-static cutscene_t CUTSCENE_TEST_SINGLE_WAIT = {
- .items = CUTSCENE_TEST_SINGLE_WAIT_ITEMS,
- .itemCount = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static void test_cutsceneSystemStartWithSetsInteractEntities(void **state) {
-
- cutsceneSystemInit();
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
-
- cutsceneSystemStartCutsceneWith(
- &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[1]
- );
-
- assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[0]);
- assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[1]);
- assert_ptr_equal(
- cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT), &ENTITIES[0]
- );
- assert_ptr_equal(
- cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED), &ENTITIES[1]
- );
-
- TIME.delta = 2.0f;
- cutsceneSystemUpdate();// ends the cutscene
- assert_null(CUTSCENE_SYSTEM.refs.entityInteract);// reset on end
- assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
-}
-
-static void test_cutsceneSystemGetEntitySentinelsRequireBeingSet(
- void **state
-) {
-
- cutsceneSystemInit();
-
- expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT));
- expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED));
- expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_CREATED));
- expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF));
-}
-
-static void test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef(
- void **state
-) {
-
- cutsceneSystemInit();
- entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
-
- entity_t *resolved = cutsceneSystemGetEntity(2);
- assert_ptr_equal(resolved, &ENTITIES[2]);
- // Resolving by direct index also updates LAST_REF.
- assert_ptr_equal(
- cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF), &ENTITIES[2]
- );
-}
-
-static void test_cutsceneSystemGetAreaId(void **state) {
-
- cutsceneSystemInit();
- // cutsceneSystemInit() zero-inits the field -- only actually starting a
- // cutscene sets it to the "nothing created yet" sentinel.
- CUTSCENE_SYSTEM.refs.areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
-
- expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED));
- assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through
-
- CUTSCENE_SYSTEM.refs.areaLastCreated = 3;
- assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED), 3);
-}
-
-static void test_cutsceneSystemGetTextMiniId(void **state) {
-
- cutsceneSystemInit();
- CUTSCENE_SYSTEM.refs.textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
-
- expect_assert_failure(
- cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED)
- );
- assert_int_equal(cutsceneSystemGetTextMiniId(4), 4);
-
- CUTSCENE_SYSTEM.refs.textMiniLastCreated = 1;
- assert_int_equal(
- cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED), 1
- );
-}
-
-static void test_cutsceneSystemDisposeResetsState(void **state) {
-
- cutsceneSystemInit();
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- cutsceneSystemStartCutsceneWith(
- &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[0]
- );
-
- cutsceneSystemDispose();
- assert_null(CUTSCENE_SYSTEM.scene);
- assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
- assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
- assert_null(CUTSCENE_SYSTEM.refs.entityInteract);
- assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
-}
-
-// --- loadedFile survival across cutsceneSystemPrepare -------------------
-//
-// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp
-// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from
-// - cutsceneSystemGoTo needs that to survive so it can reload the same source
-// later. cutsceneCutsceneResolve's caller stamps it BEFORE calling
-// cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so
-// cutsceneSystemPrepare must not blindly clear loadedFile on every
-// start - only when switching away from loadedScene to some other
-// cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This
-// regression-tests the exact bug hit via initial.jsonc's CUTSCENE item
-// into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then
-// the immediately following cutsceneSystemStartCutscene call wiped it
-// straight back out, so a later cutsceneSystemGoTo on the still-running
-// cutscene had nothing to reload from.
-static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene(
- void **state
-) {
-
- cutsceneSystemInit();
-
- // Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems
- // and stamp loadedFile with its source, before the cutscene has
- // actually started running.
- CUTSCENE_SYSTEM.loadedItems[0] =
- (cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
- CUTSCENE_SYSTEM.loadedScene = (cutscene_t){
- .items = CUTSCENE_SYSTEM.loadedItems,
- .itemCount = 1,
- .itemsMax = CUTSCENE_LOADED_ITEMS_MAX,
- .pause = CUTSCENE_PAUSE_NONE
- };
- stringCopy(
- CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc",
- ASSET_FILE_NAME_MAX - 1
- );
-
- cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
-
- assert_string_equal(
- CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc"
- );
-
- // Starting a genuinely different cutscene, though, must still
- // invalidate it - there's nothing left to reload it from.
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT);
-
- assert_string_equal(CUTSCENE_SYSTEM.loadedFile, "");
-}
-
-// --- CUTSCENE_INSERT ---------------------------------------------------
-//
-// INSERT now splices its target's items directly into the running
-// scene's own array (see cutsceneSystemInsertCutscene) rather than
-// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
-// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
-// cutscene throughout every test below. cutsceneSystemNext pops each
-// already-run item off the front of the array as it advances (see
-// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only
-// what's left to run at any given point, not a running total of
-// everything ever spliced in - the itemCount assertions below are sized
-// accordingly. Since the running scene's own array is what gets appended
-// into, any outer cutscene used with INSERT must still declare its items
-// array (and .itemsMax) with enough spare capacity for whatever ends up
-// spliced into it at once, including transitively through any nested
-// INSERT.
-
-static uint8_t insertOrderLog[8];
-static uint8_t insertOrderLogCount;
-
-static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; }
-static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; }
-static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; }
-static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; }
-static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; }
-
-static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA },
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f },
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = {
- .items = CUTSCENE_TEST_INSERT_SNIPPET_ITEMS,
- .itemCount =
- sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax =
- sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-// 5 items to start, +3 spare slots for CUTSCENE_TEST_INSERT_SNIPPET's items.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[8] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f },
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 },
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = { .cutscene = &CUTSCENE_TEST_INSERT_SNIPPET }
- },
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_OUTER = {
- .items = CUTSCENE_TEST_INSERT_OUTER_ITEMS,
- .itemCount = 5,
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_OUTER_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
-
- insertOrderLogCount = 0;
- cutsceneSystemInit();
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER);
-
- TIME.delta = 1.5f;// elapses the outer WAIT(1.0)
- cutsceneSystemUpdate();// order0 done -> starts the outer WAIT
- cutsceneSystemUpdate();// the WAIT elapses -> starts order1
- cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
-
- // The splice happened in place - CUTSCENE_SYSTEM.scene never changed.
- // order0/the WAIT/order1/the INSERT item itself have all since been
- // popped off the front, leaving the snippet's 3 items (orderA now
- // running, its WAIT, orderB) plus order2.
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
- assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4);
-
- TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0)
- cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
- cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB
- cutsceneSystemUpdate();// orderB done -> continues on to order2
-
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
-
- cutsceneSystemUpdate();// order2 done -> cutscene ends naturally
- assert_null(CUTSCENE_SYSTEM.scene);
-
- const uint8_t expected[] = { 0, 1, 3, 4, 2 };
- assert_int_equal(insertOrderLogCount, sizeof(expected));
- assert_memory_equal(insertOrderLog, expected, sizeof(expected));
-}
-
-static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = {
- .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS,
- .itemCount = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
- sizeof(cutsceneitem_t),
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
- sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-// 2 items to start, +1 spare slot for the snippet's single item.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[3] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = {
- .cutscene = &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET
- }
- }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER = {
- .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS,
- .itemCount = 2,
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS) /
- sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
- void **state
-) {
-
- insertOrderLogCount = 0;
- cutsceneSystemInit();
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
-
- cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
- // order0 and the INSERT item itself have both been popped off the
- // front by now, leaving just orderA (currently running).
- assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
-
- // orderA is the snippet's only item, and the snippet was spliced in as
- // the outer's last item -- finishing it should reach the natural end
- // of the whole cutscene, not get stuck.
- cutsceneSystemUpdate();
- assert_null(CUTSCENE_SYSTEM.scene);
-
- const uint8_t expected[] = { 0, 3 };
- assert_int_equal(insertOrderLogCount, sizeof(expected));
- assert_memory_equal(insertOrderLog, expected, sizeof(expected));
-}
-
-static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = {
- .items = CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS,
- .itemCount =
- sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax =
- sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-// INSERT is MID's first item, so entering MID (from OUTER) cascades
-// straight through into INNER within the same Start chain, one level
-// deeper than a single insert. MID is only ever read from as a splice
-// source (see CUTSCENE_TEST_INSERT_NESTED_OUTER) - it's never itself the
-// running scene, so it needs no spare capacity of its own.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = {
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER }
- },
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = {
- .items = CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS,
- .itemCount =
- sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
- .itemsMax =
- sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-// 3 items to start, +2 spare for MID's items, +1 more for MID's own
-// nested INSERT(INNER) once MID's items become part of this array too.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[6] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_MID }
- },
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_NESTED_OUTER = {
- .items = CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS,
- .itemCount = 3,
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS) /
- sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
-
- insertOrderLogCount = 0;
- cutsceneSystemInit();
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER);
-
- // order0 done -> starts INSERT(MID), cascading through INSERT(INNER) ->
- // orderA, all within this one Update call's Start chain - both splices
- // land in OUTER's own array.
- cutsceneSystemUpdate();
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
- // order0 and both INSERT items themselves have all been popped off the
- // front by now, leaving orderA (currently running), orderB, order2.
- assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
-
- cutsceneSystemUpdate();// orderA done -> continues on to orderB
- cutsceneSystemUpdate();// orderB done -> continues on to order2
- cutsceneSystemUpdate();// order2 done -> ends naturally
- assert_null(CUTSCENE_SYSTEM.scene);
-
- const uint8_t expected[] = { 0, 3, 4, 2 };
- assert_int_equal(insertOrderLogCount, sizeof(expected));
- assert_memory_equal(insertOrderLog, expected, sizeof(expected));
-}
-
-static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET = {
- .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS,
- .itemCount = 1,
- .itemsMax = 1,
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-// No spare capacity at all - itemsMax equals itemCount, leaving no room
-// to splice anything into it.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS[1] = {
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET }
- }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER = {
- .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS,
- .itemCount = 1,
- .itemsMax = 1,
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static void test_cutsceneInsertAssertsWhenExceedingItemsMax(void **state) {
-
- cutsceneSystemInit();
-
- expect_assert_failure(
- cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER)
- );
-}
-
-static void noopInsertCallback(void *userData) {}
-
-static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = {
- .items = CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS,
- .itemCount = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
- sizeof(cutsceneitem_t),
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
- sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_ALL
-};
-
-// 1 item to start, +1 spare slot for the snippet's single item.
-static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[2] = {
- {
- .type = CUTSCENE_ITEM_TYPE_INSERT,
- .insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET }
- }
-};
-static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_OUTER = {
- .items = CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS,
- .itemCount = 1,
- .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS) /
- sizeof(cutsceneitem_t),
- .pause = CUTSCENE_PAUSE_NPC
-};
-
-static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities(
- void **state
-) {
-
- cutsceneSystemInit();
- entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER);
- entityInit(&ENTITIES[4], ENTITY_TYPE_NPC);
-
- cutsceneSystemStartCutsceneWith(
- &CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4]
- );
-
- // The INSERT item's Start already spliced the snippet into place and
- // cascaded into it by now - and since the INSERT item itself has since
- // been popped off the front, only the snippet's one item is left.
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER);
- assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
- assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[3]);
- assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[4]);
- // The snippet declares ALL, but INSERT doesn't apply it -- the outer
- // cutscene's NPC pause, already in effect, stands.
- assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC);
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem),
- cmocka_unit_test(test_cutsceneSystemNestedCutsceneIsOneWayJump),
- cmocka_unit_test(test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene),
- cmocka_unit_test(test_cutsceneSystemStartWithSetsInteractEntities),
- cmocka_unit_test(test_cutsceneSystemGetEntitySentinelsRequireBeingSet),
- cmocka_unit_test(test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef),
- cmocka_unit_test(test_cutsceneSystemGetAreaId),
- cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
- cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
- cmocka_unit_test(
- test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
- ),
- cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
- cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
- cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
- cmocka_unit_test(test_cutsceneInsertAssertsWhenExceedingItemsMax),
- cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/entity/CMakeLists.txt b/test/rpg/entity/CMakeLists.txt
deleted file mode 100644
index 453ff3da..00000000
--- a/test/rpg/entity/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-include(dusktest)
-
-# Tests
-dusktest(test_entity.c)
-target_sources(test_entity PRIVATE entitytestfixture.c)
-
-dusktest(test_npc.c)
-target_sources(test_npc PRIVATE entitytestfixture.c)
-
-dusktest(test_entityinteract.c)
-target_sources(test_entityinteract PRIVATE entitytestfixture.c)
-
-dusktest(test_entityitem.c)
-target_sources(test_entityitem PRIVATE entitytestfixture.c)
diff --git a/test/rpg/entity/entitytestfixture.c b/test/rpg/entity/entitytestfixture.c
deleted file mode 100644
index bbc9d447..00000000
--- a/test/rpg/entity/entitytestfixture.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "entitytestfixture.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/tile.h"
-#include "util/memory.h"
-#include "util/string.h"
-
-void entityTestFixtureReset(void) {
- memoryZero(ENTITIES, sizeof(ENTITIES));
-
- memoryZero(&MAP, sizeof(map_t));
- stringCopy(MAP.name, "test", MAP_NAME_MAX);
- MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
-
- chunk_t *chunk = &MAP.chunks[0];
- chunk->position = (chunkpos_t){ 0, 0, 0 };
- for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
- chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
- }
- for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
- chunk->entities[i] = 0xFF;
- }
-
- MAP.chunkOrder[0] = chunk;
-}
diff --git a/test/rpg/entity/entitytestfixture.h b/test/rpg/entity/entitytestfixture.h
deleted file mode 100644
index 89e591e4..00000000
--- a/test/rpg/entity/entitytestfixture.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-#include "rpg/entity/entity.h"
-
-/**
- * Resets ENTITIES[] to all-empty and builds a single minimal, synchronously
- * "loaded" chunk at chunk position (0, 0, 0) on the MAP global, entirely
- * TILE_SHAPE_GROUND at local Z 0. Sufficient for entityWalk/Turn/Run, chunk
- * bookkeeping, and NPC movement tests -- does not exercise the real async
- * chunk-loading pipeline (mapChunkLoad).
- *
- * World positions with x/y in [0, CHUNK_WIDTH-1]/[0, CHUNK_HEIGHT-1] and
- * z == 0 fall within the fixture chunk. Individual tests may override
- * specific columns afterward (e.g. to place a ramp).
- */
-void entityTestFixtureReset(void);
diff --git a/test/rpg/entity/test_entity.c b/test/rpg/entity/test_entity.c
deleted file mode 100644
index 54a2bcf0..00000000
--- a/test/rpg/entity/test_entity.c
+++ /dev/null
@@ -1,300 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "entitytestfixture.h"
-#include "rpg/overworld/map.h"
-#include "rpg/overworld/worldpos.h"
-
-static void setTile(
- const worldpos_t pos, const tileshape_t shape, const uint8_t z
-) {
- chunktileindex_t index = worldPosToChunkTileIndex(&pos);
- MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = z };
-}
-
-static void test_entityInit(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[3], ENTITY_TYPE_ITEM);
- assert_int_equal(ENTITIES[3].id, 3);
- assert_int_equal(ENTITIES[3].type, ENTITY_TYPE_ITEM);
- assert_int_equal(ENTITIES[3].chunkIndex, 0xFF);
- assert_int_equal(ENTITIES[3].globalId, ENTITY_GLOBAL_ID_NULL);
- // entityItemInit wires the interact callback -- confirms init dispatch ran.
- assert_int_equal(ENTITIES[3].interact.type, ENTITY_INTERACT_CALLBACK);
-
- expect_assert_failure(entityInit(NULL, ENTITY_TYPE_ITEM));
- expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_NULL));
- expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_COUNT));
-
- entity_t offBounds;
- expect_assert_failure(entityInit(&offBounds, ENTITY_TYPE_ITEM));
-}
-
-static void test_entityGetAvailable(void **state) {
-
- entityTestFixtureReset();
-
- assert_int_equal(entityGetAvailable(), 0);
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- assert_int_equal(entityGetAvailable(), 1);
- entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
- assert_int_equal(entityGetAvailable(), 2);
-}
-
-static void test_entityGetAt(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 3, 4, 0 };
-
- assert_ptr_equal(entityGetAt((worldpos_t){ 3, 4, 0 }), &ENTITIES[0]);
- assert_null(entityGetAt((worldpos_t){ 3, 4, 1 }));
- assert_null(entityGetAt((worldpos_t){ 0, 0, 0 }));
-}
-
-static void test_entityGetByGlobalId(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
- ENTITIES[2].globalId = 42;
-
- assert_ptr_equal(entityGetByGlobalId(42), &ENTITIES[2]);
- assert_null(entityGetByGlobalId(43));
- assert_null(entityGetByGlobalId(ENTITY_GLOBAL_ID_NULL));
-}
-
-static void test_entityCanUnload(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START - 1;
- assert_true(entityCanUnload(&ENTITIES[0]));
-
- ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START;
- assert_false(entityCanUnload(&ENTITIES[0]));
-}
-
-static void test_entityCanTurnWalkRun(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- assert_true(entityCanTurn(&ENTITIES[0]));
- assert_true(entityCanWalk(&ENTITIES[0]));
- assert_true(entityCanRun(&ENTITIES[0]));
-
- ENTITIES[0].animation = ENTITY_ANIM_WALK;
- assert_false(entityCanTurn(&ENTITIES[0]));
- assert_false(entityCanWalk(&ENTITIES[0]));
- assert_false(entityCanRun(&ENTITIES[0]));
-
- ENTITIES[0].animation = ENTITY_ANIM_IDLE;
- ENTITIES[0].walkEndCooldown = 0.1f;
- assert_false(entityCanTurn(&ENTITIES[0]));// cooldown blocks turning only
- assert_true(entityCanWalk(&ENTITIES[0]));
-}
-
-static void test_entityTurn(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- entityTurn(&ENTITIES[0], ENTITY_DIR_EAST);
- assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_EAST);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_TURN);
-
- // Can't turn again mid-turn.
- ENTITIES[0].direction = ENTITY_DIR_NORTH;
- entityTurn(&ENTITIES[0], ENTITY_DIR_WEST);
- assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
-}
-
-static void test_entityWalkOnOpenGround(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
-
- entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
- assert_int_equal(ENTITIES[0].position.x, 5);
- assert_int_equal(ENTITIES[0].position.y, 6);
- assert_int_equal(ENTITIES[0].position.z, 0);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_WALK);
- assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
- // entityUpdateChunk should have assigned it to the fixture chunk.
- assert_int_equal(ENTITIES[0].chunkIndex, 0);
- assert_int_equal(MAP.chunks[0].entities[0], 0);
-}
-
-static void test_entityWalkBlockedAtChunkEdge(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 0, 0, 0 };
-
- // West/south of the loaded chunk is unloaded (TILE_NULL) -- blocked.
- entityWalk(&ENTITIES[0], ENTITY_DIR_WEST);
- assert_int_equal(ENTITIES[0].position.x, 0);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
-}
-
-static void test_entityWalkBlockedByOtherEntity(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
-
- entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
- ENTITIES[1].position = (worldpos_t){ 5, 6, 0 };// directly north
-
- entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
- assert_int_equal(ENTITIES[0].position.x, 5);
- assert_int_equal(ENTITIES[0].position.y, 5);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
-}
-
-static void test_entityWalkCannotWhileNotIdle(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
- ENTITIES[0].animation = ENTITY_ANIM_WALK;
-
- entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
- assert_int_equal(ENTITIES[0].position.y, 5);// unchanged
-}
-
-// Ramp at (5,5) facing north, raised GROUND at (5,6)/z1 -- walking north
-// off the ramp raises the entity by one Z layer, and walking back south
-// from the raised tile falls back down onto the ramp.
-static void setUpRamp(void) {
- setTile((worldpos_t){ 5, 5, 0 }, TILE_SHAPE_RAMP_NORTH, 0);
- setTile((worldpos_t){ 5, 6, 0 }, TILE_SHAPE_GROUND, 1);
-}
-
-static void test_entityWalkUpRamp(void **state) {
-
- entityTestFixtureReset();
- setUpRamp();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
-
- entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
- assert_int_equal(ENTITIES[0].position.x, 5);
- assert_int_equal(ENTITIES[0].position.y, 6);
- assert_int_equal(ENTITIES[0].position.z, 1);
-}
-
-static void test_entityWalkFallDownRamp(void **state) {
-
- entityTestFixtureReset();
- setUpRamp();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 6, 1 };
-
- entityWalk(&ENTITIES[0], ENTITY_DIR_SOUTH);
- assert_int_equal(ENTITIES[0].position.x, 5);
- assert_int_equal(ENTITIES[0].position.y, 5);
- assert_int_equal(ENTITIES[0].position.z, 0);
-}
-
-static void test_entityWalkCannotClimbRampFromTheSide(void **state) {
-
- entityTestFixtureReset();
- setUpRamp();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
-
- // Only NORTH climbs this ramp -- EAST off the ramp tile is just blocked
- // ground movement, not a climb.
- entityWalk(&ENTITIES[0], ENTITY_DIR_EAST);
- assert_int_equal(ENTITIES[0].position.x, 6);
- assert_int_equal(ENTITIES[0].position.z, 0);
-}
-
-static void test_entityRun(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
-
- entityRun(&ENTITIES[0], ENTITY_DIR_NORTH);
- assert_int_equal(ENTITIES[0].position.y, 6);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_RUN);
-}
-
-static void test_entitySetChunkAndUpdateChunk(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- entitySetChunk(&ENTITIES[0], 0);
- assert_int_equal(ENTITIES[0].chunkIndex, 0);
- assert_int_equal(MAP.chunks[0].entities[0], 0);
-
- entitySetChunk(&ENTITIES[0], 0xFF);
- assert_int_equal(ENTITIES[0].chunkIndex, 0xFF);
- assert_int_equal(MAP.chunks[0].entities[0], 0xFF);
-
- ENTITIES[0].position = (worldpos_t){ 3, 3, 0 };
- entityUpdateChunk(&ENTITIES[0]);
- assert_int_equal(ENTITIES[0].chunkIndex, 0);
-}
-
-static void test_entityPositionSet(void **state) {
-
- entityTestFixtureReset();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].animation = ENTITY_ANIM_WALK;
- ENTITIES[0].walkEndCooldown = 5;
-
- entityPositionSet(&ENTITIES[0], (worldpos_t){ 7, 8, 0 });
- assert_int_equal(ENTITIES[0].position.x, 7);
- assert_int_equal(ENTITIES[0].position.y, 8);
- assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
- assert_int_equal(ENTITIES[0].walkEndCooldown, 0);
- assert_int_equal(ENTITIES[0].chunkIndex, 0);// updated as a side effect
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_entityInit),
- cmocka_unit_test(test_entityGetAvailable),
- cmocka_unit_test(test_entityGetAt),
- cmocka_unit_test(test_entityGetByGlobalId),
- cmocka_unit_test(test_entityCanUnload),
- cmocka_unit_test(test_entityCanTurnWalkRun),
- cmocka_unit_test(test_entityTurn),
- cmocka_unit_test(test_entityWalkOnOpenGround),
- cmocka_unit_test(test_entityWalkBlockedAtChunkEdge),
- cmocka_unit_test(test_entityWalkBlockedByOtherEntity),
- cmocka_unit_test(test_entityWalkCannotWhileNotIdle),
- cmocka_unit_test(test_entityWalkUpRamp),
- cmocka_unit_test(test_entityWalkFallDownRamp),
- cmocka_unit_test(test_entityWalkCannotClimbRampFromTheSide),
- cmocka_unit_test(test_entityRun),
- cmocka_unit_test(test_entitySetChunkAndUpdateChunk),
- cmocka_unit_test(test_entityPositionSet),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/entity/test_entityinteract.c b/test/rpg/entity/test_entityinteract.c
deleted file mode 100644
index 4980e21f..00000000
--- a/test/rpg/entity/test_entityinteract.c
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "entitytestfixture.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "ui/focus/uifocus.h"
-#include "rpg/cutscene/cutscenesystem.h"
-
-static cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = {
- { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
-};
-static cutscene_t CUTSCENE_TEST_INTERACT = {
- .items = CUTSCENE_TEST_INTERACT_ITEMS,
- .itemCount = 1,
- .itemsMax = 1,
- .pause = CUTSCENE_PAUSE_NONE
-};
-
-static entity_t *player;
-static entity_t *target;
-
-static void resetInteractFixture(void) {
- entityTestFixtureReset();
- uiFocusInit();
- uiTextboxMainInit();
- uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
- cutsceneSystemInit();
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
- player = &ENTITIES[0];
- target = &ENTITIES[1];
- player->direction = ENTITY_DIR_NORTH;
-}
-
-static void test_entityInteractWithNull(void **state) {
-
- resetInteractFixture();
-
- target->interact.type = ENTITY_INTERACT_NULL;
- entityInteractWith(player, target);// should not crash or change anything
- assert_int_equal(target->interact.type, ENTITY_INTERACT_NULL);
-}
-
-static void test_entityInteractWithPrintTurnsNpcToFacePlayer(void **state) {
-
- resetInteractFixture();
-
- target->interact.type = ENTITY_INTERACT_PRINT;
- target->interact.data.message = "Hello!";
-
- entityInteractWith(player, target);
-
- assert_true(uiTextboxMainIsActive());
- assert_int_equal(
- target->data.npc.interactState, NPC_INTERACT_STATE_CONVERSING
- );
- // NPC turns to face the opposite of the player's facing direction.
- assert_int_equal(target->direction, entityDirGetOpposite(player->direction));
-}
-
-static entity_t *callbackPlayerArg;
-static entity_t *callbackTargetArg;
-static uint8_t callbackCount;
-
-static void recordInteractCallback(entity_t *p, entity_t *t) {
- callbackPlayerArg = p;
- callbackTargetArg = t;
- callbackCount++;
-}
-
-static void test_entityInteractWithCallback(void **state) {
-
- resetInteractFixture();
- callbackCount = 0;
-
- target->interact.type = ENTITY_INTERACT_CALLBACK;
- target->interact.data.callback = recordInteractCallback;
-
- entityInteractWith(player, target);
-
- assert_int_equal(callbackCount, 1);
- assert_ptr_equal(callbackPlayerArg, player);
- assert_ptr_equal(callbackTargetArg, target);
-}
-
-static void test_entityInteractWithCallbackRequiresNonNull(void **state) {
-
- resetInteractFixture();
-
- target->interact.type = ENTITY_INTERACT_CALLBACK;
- target->interact.data.callback = NULL;
-
- expect_assert_failure(entityInteractWith(player, target));
-}
-
-static void test_entityInteractWithCutsceneStartsIt(void **state) {
-
- resetInteractFixture();
-
- target->interact.type = ENTITY_INTERACT_CUTSCENE;
- target->interact.data.cutscene = &CUTSCENE_TEST_INTERACT;
-
- entityInteractWith(player, target);
-
- assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INTERACT);
- assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, player);
- assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, target);
-}
-
-static void test_entityInteractWithRequiresNonNullEntities(void **state) {
-
- resetInteractFixture();
-
- expect_assert_failure(entityInteractWith(NULL, target));
- expect_assert_failure(entityInteractWith(player, NULL));
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_entityInteractWithNull),
- cmocka_unit_test(test_entityInteractWithPrintTurnsNpcToFacePlayer),
- cmocka_unit_test(test_entityInteractWithCallback),
- cmocka_unit_test(test_entityInteractWithCallbackRequiresNonNull),
- cmocka_unit_test(test_entityInteractWithCutsceneStartsIt),
- cmocka_unit_test(test_entityInteractWithRequiresNonNullEntities),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/entity/test_entityitem.c b/test/rpg/entity/test_entityitem.c
deleted file mode 100644
index f4936ec2..00000000
--- a/test/rpg/entity/test_entityitem.c
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "entitytestfixture.h"
-#include "ui/rpg/textbox/uitextboxmain.h"
-#include "ui/focus/uifocus.h"
-
-// An arbitrary, non-null itemid_t -- entityItemSet just stores/reports it
-// verbatim, it doesn't need a real item registered in ITEM.
-#define TEST_ITEM_ID ((itemid_t)1)
-
-static entity_t *setUpItemEntity(void) {
- entityTestFixtureReset();
- uiFocusInit();
- uiTextboxMainInit();
- uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_ITEM);
- return &ENTITIES[0];
-}
-
-static void test_entityItemInit(void **state) {
-
- entity_t *item = setUpItemEntity();
-
- assert_int_equal(item->interact.type, ENTITY_INTERACT_CALLBACK);
- assert_ptr_equal(item->interact.data.callback, entityItemInteract);
-}
-
-static void test_entityItemSet(void **state) {
-
- entity_t *item = setUpItemEntity();
-
- entityItemSet(item, TEST_ITEM_ID, 3);
- assert_int_equal(item->data.item.item, TEST_ITEM_ID);
- assert_int_equal(item->data.item.quantity, 3);
- assert_false(item->data.item.collected);
-}
-
-// entityItemInteract itself is NOT covered here: it unconditionally calls
-// itemGive(), which calls itemGetName(), which dereferences
-// LOCALE.entry->data.locale -- a real locale asset populated only by the
-// async asset-loading pipeline (see localemanager.h/assetlocaleloader.h).
-// That's exactly the asset-loading dependency this test pass scoped out;
-// faking it cheaply isn't possible without hand-building the loader's
-// internal hash format, which is more coupling than it's worth. Backpack
-// bookkeeping itself is already covered by test/item/test_inventory.c-style
-// tests at the inventory/backpack layer.
-
-static void test_entityItemMovementWaitsForCollectionAndTextbox(void **state) {
-
- entity_t *item = setUpItemEntity();
-
- // Not collected yet -- stays.
- entityItemMovement(item);
- assert_int_equal(item->type, ENTITY_TYPE_ITEM);
-
- // Collected (set directly -- see note above on why entityItemInteract
- // itself isn't driven here), but a message is still showing -- stays.
- item->data.item.collected = true;
- uiTextboxMainSetText("Picked up!");
- entityItemMovement(item);
- assert_int_equal(item->type, ENTITY_TYPE_ITEM);
-
- // Once the textbox is dismissed, the entity despawns.
- uiFocusPop();
- assert_false(uiTextboxMainIsActive());
- entityItemMovement(item);
- assert_int_equal(item->type, ENTITY_TYPE_NULL);
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_entityItemInit),
- cmocka_unit_test(test_entityItemSet),
- cmocka_unit_test(test_entityItemMovementWaitsForCollectionAndTextbox),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/entity/test_npc.c b/test/rpg/entity/test_npc.c
deleted file mode 100644
index aaa5174b..00000000
--- a/test/rpg/entity/test_npc.c
+++ /dev/null
@@ -1,155 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "entitytestfixture.h"
-#include "rpg/cutscene/cutscenesystem.h"
-#include "time/time.h"
-
-static entity_t *setUpNpc(const worldpos_t position) {
- entityTestFixtureReset();
- entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
- ENTITIES[0].position = position;
- return &ENTITIES[0];
-}
-
-static void test_npcSetMoveType(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
-
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
- assert_int_equal(npc->data.npc.moveType, NPC_MOVE_TYPE_RANDOM_TURN);
- // Init ran: timer was seeded within the default frequency range.
- assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
-}
-
-static void test_npcRandomTurnMovementFiresOnceTimerElapses(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
- npc->data.npc.moveData.randomTurn.timer = 0.05f;
-
- TIME.delta = 0.01f;
- npcRandomTurnMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// timer not elapsed yet
-
- TIME.delta = 1.0f;
- npcRandomTurnMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_TURN);
- // Timer is reseeded, not left at/below zero.
- assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
-}
-
-static void test_npcRandomWalkMovementFiresOnceTimerElapses(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
- npc->data.npc.moveData.randomWalk.timer = 0.05f;
-
- TIME.delta = 0.01f;
- npcRandomWalkMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
-
- TIME.delta = 1.0f;
- npcRandomWalkMovement(npc);
- // Open ground on all sides -- whichever direction is chosen, it moves.
- assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
-}
-
-static void test_npcRandomTurnAndWalkMovementRunsBothIndependently(
- void **state
-) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK);
- npcrandomturnandwalk_t *tw = &npc->data.npc.moveData.randomTurnAndWalk;
- tw->turn.timer = 999.0f;// don't fire this tick
- tw->walk.timer = 0.05f;// fires this tick
-
- TIME.delta = 1.0f;
- npcRandomTurnAndWalkMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
- assert_true(tw->turn.timer > 900.0f);// unaffected by the walk firing
-}
-
-static void test_npcMovementGatedByCutscenePause(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
- npc->data.npc.moveData.randomWalk.timer = 0.0f;
- TIME.delta = 1.0f;
-
- CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NPC;
- npcMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked by pause
-
- CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
- npcMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
-}
-
-static void test_npcMovementGatedByInteractState(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
- npc->data.npc.moveData.randomWalk.timer = 0.0f;
- TIME.delta = 1.0f;
-
- npc->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
- npcMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked while conversing
-
- npc->data.npc.interactState = NPC_INTERACT_STATE_NONE;
- npcMovement(npc);
- assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
-}
-
-static void test_npcPathMovementFollowsAndLoopsWaypoints(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
- npcpath_t *path = &npc->data.npc.moveData.path;
- npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 6, 0 });
- npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 5, 0 });
-
- npcPathMovement(npc);// steps toward waypoint 0
- assert_int_equal(npc->position.x, 5);
- assert_int_equal(npc->position.y, 6);
- assert_int_equal(path->index, 0);// not yet considered "arrived"
-
- npc->animation = ENTITY_ANIM_IDLE;// simulate the walk animation finishing
- npcPathMovement(npc);// arrives at waypoint 0, advances, steps toward wp 1
- assert_int_equal(path->index, 1);
- assert_int_equal(npc->position.x, 5);
- assert_int_equal(npc->position.y, 5);
-}
-
-static void test_npcPathMovementNoopWithEmptyPath(void **state) {
-
- entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
- npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
-
- npcPathMovement(npc);
- assert_int_equal(npc->position.x, 5);
- assert_int_equal(npc->position.y, 5);
- assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_npcSetMoveType),
- cmocka_unit_test(test_npcRandomTurnMovementFiresOnceTimerElapses),
- cmocka_unit_test(test_npcRandomWalkMovementFiresOnceTimerElapses),
- cmocka_unit_test(test_npcRandomTurnAndWalkMovementRunsBothIndependently),
- cmocka_unit_test(test_npcMovementGatedByCutscenePause),
- cmocka_unit_test(test_npcMovementGatedByInteractState),
- cmocka_unit_test(test_npcPathMovementFollowsAndLoopsWaypoints),
- cmocka_unit_test(test_npcPathMovementNoopWithEmptyPath),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/overworld/CMakeLists.txt b/test/rpg/overworld/CMakeLists.txt
deleted file mode 100644
index 7f8ecc94..00000000
--- a/test/rpg/overworld/CMakeLists.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2026 Dominic Masters
-#
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-include(dusktest)
-
-# Tests
-dusktest(test_maparea.c)
-
-# Subdirs
\ No newline at end of file
diff --git a/test/rpg/overworld/test_maparea.c b/test/rpg/overworld/test_maparea.c
deleted file mode 100644
index 259200af..00000000
--- a/test/rpg/overworld/test_maparea.c
+++ /dev/null
@@ -1,217 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-#include "rpg/overworld/maparea.h"
-#include "rpg/overworld/map.h"
-#include "rpg/entity/entity.h"
-#include "util/memory.h"
-
-static uint8_t callbackCount;
-static uint8_t lastTrigger;
-
-static void recordCallback(entity_t *entity, const uint8_t trigger) {
- callbackCount++;
- lastTrigger = trigger;
-}
-
-static void resetMapAreas(void) {
- memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
- callbackCount = 0;
- lastTrigger = 0;
-}
-
-static void test_mapAreaInit(void **state) {
-
-
- maparea_t area;
- const worldpos_t min = { 5, 5, 0 };
- const worldpos_t max = { 0, 0, 0 };
-
- // min/max should be normalized regardless of argument order.
- mapAreaInit(
- &area, min, max, recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
- assert_int_equal(area.min.x, 0);
- assert_int_equal(area.min.y, 0);
- assert_int_equal(area.max.x, 5);
- assert_int_equal(area.max.y, 5);
- assert_int_equal(area.triggerCount, 0);
-
- expect_assert_failure(
- mapAreaInit(&area, min, max, NULL, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL)
- );
-}
-
-static void test_mapAreaIsInside(void **state) {
-
-
- maparea_t area;
- mapAreaInit(
- &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- assert_true(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 0 }));
- assert_true(mapAreaIsInside(&area, (worldpos_t){ 0, 0, 0 }));// inclusive min
- assert_true(
- mapAreaIsInside(&area, (worldpos_t){ 10, 10, 0 })
- );// inclusive max
- assert_false(mapAreaIsInside(&area, (worldpos_t){ 11, 5, 0 }));
- assert_false(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 1 }));
-}
-
-static void test_mapAreaShouldNotify(void **state) {
-
-
- maparea_t area;
- mapAreaInit(
- &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
- recordCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
- );
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
- entityInit(&ENTITIES[2], ENTITY_TYPE_ITEM);
-
- assert_true(mapAreaShouldNotify(&area, &ENTITIES[0]));
- assert_false(mapAreaShouldNotify(&area, &ENTITIES[1]));// not notified
- assert_false(mapAreaShouldNotify(&area, &ENTITIES[2]));// item never notifies
-
- area.notify = MAP_AREA_NOTIFY_ALL;
- assert_true(mapAreaShouldNotify(&area, &ENTITIES[1]));
-}
-
-static void test_mapAreaAddAndRemove(void **state) {
-
- resetMapAreas();
-
- uint8_t id = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
- assert_int_equal(id, 0);
- assert_ptr_equal(MAP_AREAS[0].callback, recordCallback);
-
- // Adding again should reuse the next free slot, not the same one.
- uint8_t id2 = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
- assert_int_equal(id2, 1);
-
- mapAreaRemove(id);
- assert_null(MAP_AREAS[0].callback);// slot freed
-
- // Removing frees the slot for reuse.
- uint8_t id3 = mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
- assert_int_equal(id3, 0);
-}
-
-static void test_mapAreaCheckEntityTriggersEnterStepExit(void **state) {
-
- resetMapAreas();
-
- mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };// outside
-
- // Outside -> outside: no callback.
- mapAreaCheckEntity(&ENTITIES[0]);
- assert_int_equal(callbackCount, 0);
- assert_int_equal(MAP_AREAS[0].triggerCount, 0);
-
- // Outside -> inside: ENTER.
- ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
- mapAreaCheckEntity(&ENTITIES[0]);
- assert_int_equal(callbackCount, 1);
- assert_int_equal(lastTrigger, MAP_TRIGGER_ENTER);
- assert_int_equal(MAP_AREAS[0].triggerCount, 1);
-
- // Inside -> inside: STEP, every subsequent call.
- mapAreaCheckEntity(&ENTITIES[0]);
- assert_int_equal(callbackCount, 2);
- assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
-
- mapAreaCheckEntity(&ENTITIES[0]);
- assert_int_equal(callbackCount, 3);
- assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
-
- // Inside -> outside: EXIT.
- ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
- mapAreaCheckEntity(&ENTITIES[0]);
- assert_int_equal(callbackCount, 4);
- assert_int_equal(lastTrigger, MAP_TRIGGER_EXIT);
-}
-
-static void test_mapAreaCheckEntityRespectsTriggerMask(void **state) {
-
- resetMapAreas();
-
- // Only interested in ENTER, not STEP or EXIT.
- mapAreaAdd(
- (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ENTER
- );
-
- entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
- ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
-
- mapAreaCheckEntity(&ENTITIES[0]);// ENTER: fires
- assert_int_equal(callbackCount, 1);
-
- mapAreaCheckEntity(&ENTITIES[0]);// STEP: masked out
- assert_int_equal(callbackCount, 1);
-
- ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
- mapAreaCheckEntity(&ENTITIES[0]);// EXIT: masked out
- assert_int_equal(callbackCount, 1);
-}
-
-static void test_mapAreaCanUnload(void **state) {
-
- resetMapAreas();
- memoryZero(&MAP, sizeof(map_t));
-
- maparea_t area;
- mapAreaInit(
- &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
- recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
- );
-
- // No chunk overlaps the area's bounds (all chunks default to position 0,
- // 0, 0 after memoryZero -- move them far away first).
- for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
- MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
- }
- assert_true(mapAreaCanUnload(&area));
-
- // One chunk overlapping the area's bounds blocks unload.
- MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
- assert_false(mapAreaCanUnload(&area));
-}
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_mapAreaInit),
- cmocka_unit_test(test_mapAreaIsInside),
- cmocka_unit_test(test_mapAreaShouldNotify),
- cmocka_unit_test(test_mapAreaAddAndRemove),
- cmocka_unit_test(test_mapAreaCheckEntityTriggersEnterStepExit),
- cmocka_unit_test(test_mapAreaCheckEntityRespectsTriggerMask),
- cmocka_unit_test(test_mapAreaCanUnload),
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
diff --git a/test/rpg/test_rpg.c b/test/rpg/test_rpg.c
deleted file mode 100644
index 7a5e36d7..00000000
--- a/test/rpg/test_rpg.c
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) 2026 Dominic Masters
- *
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "dusktest.h"
-
-int main(int argc, char** argv) {
- const struct CMUnitTest tests[] = {
- // Add RPG tests here in the future
- };
-
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
\ No newline at end of file
diff --git a/test/save/savetestfixture.c b/test/save/savetestfixture.c
index eb1aee01..8f14bfe8 100644
--- a/test/save/savetestfixture.c
+++ b/test/save/savetestfixture.c
@@ -10,9 +10,6 @@
#include "savetestfixture.h"
#include "assert/assert.h"
-#include "asset/asset.h"
-#include "asset/loader/assetloader.h"
-#include "asset/loader/assetentry.h"
#include "util/memory.h"
#include "util/string.h"
#include
@@ -25,51 +22,6 @@ static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
-static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON =
- "{\"members\":{\"hero\":{"
- "\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0,"
- "\"healthMax\":30,\"mpMax\":10"
- "}}}";
-
-static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
-
-errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) {
- loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC;
- loading->entry->state = ASSET_ENTRY_STATE_LOADED;
- errorOk();
-}
-
-errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) {
- errorOk();
-}
-
-void saveTestFixtureStubPartyAsset(void) {
- memoryZero(&ASSET, sizeof(ASSET));
- for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
- threadMutexInit(&ASSET.loading[i].mutex);
- }
-
- if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) {
- SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read(
- SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0
- );
- assertNotNull(
- SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture"
- );
- }
-
- ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync =
- saveTestFixturePartyJsonLoadSyncStub;
- ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose =
- saveTestFixturePartyJsonDisposeStub;
-}
-
-void saveTestFixtureUnstubPartyAsset(void) {
- if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return;
- yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC);
- SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
-}
-
static int_t saveTestFixtureRemoveEntry(
const char *path,
const struct stat *statBuffer,
@@ -97,8 +49,6 @@ int saveTestFixtureSetup(void **state) {
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
- saveTestFixtureStubPartyAsset();
-
errorret_t ret = saveInit();
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
@@ -106,7 +56,6 @@ int saveTestFixtureSetup(void **state) {
}
int saveTestFixtureTeardown(void **state) {
- saveTestFixtureUnstubPartyAsset();
nftw(
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
diff --git a/test/save/savetestfixture.h b/test/save/savetestfixture.h
index 2ac2bde8..59f65fff 100644
--- a/test/save/savetestfixture.h
+++ b/test/save/savetestfixture.h
@@ -36,20 +36,3 @@ int saveTestFixtureTeardown(void **state);
*/
void saveTestFixtureBlockSaveDirectory(void);
-/**
- * Stubs the JSON asset loader's sync callback to hand back a small
- * in-memory party.jsonc-shaped doc instead of doing a real file/zip read -
- * saveInit()/saveSlotInit() seed the party roster via partyInit(), which
- * loads party.jsonc through the real asset system, and there's no real
- * asset bundle available under the test harness. No thread, no real I/O.
- * Resets ASSET to a fresh, otherwise-empty state each call, so it's safe
- * to call once per test. Called automatically by saveTestFixtureSetup.
- */
-void saveTestFixtureStubPartyAsset(void);
-
-/**
- * Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe
- * to call even if that function was never called. Called automatically
- * by saveTestFixtureTeardown.
- */
-void saveTestFixtureUnstubPartyAsset(void);
diff --git a/test/save/test_saveslot.c b/test/save/test_saveslot.c
index fab7b8a0..44fdd67d 100644
--- a/test/save/test_saveslot.c
+++ b/test/save/test_saveslot.c
@@ -15,19 +15,6 @@
#include
#include
-// saveSlotInit() seeds the party roster via partyInit(), which loads
-// party.jsonc through the real asset system - stub it out (see
-// savetestfixture.h) rather than needing a real asset bundle here.
-static int assetGroupSetup(void **state) {
- saveTestFixtureStubPartyAsset();
- return 0;
-}
-
-static int assetGroupTeardown(void **state) {
- saveTestFixtureUnstubPartyAsset();
- return 0;
-}
-
// ============================================================
// Helpers - wrap the writeInit/readInit macros (which can only be used
// inside a function returning errorret_t) so tests can call them plainly.
@@ -156,12 +143,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 42;
- // An in-use slot (non-empty name) requires a non-empty map name too -
- // see saveSlotNewGame/requireStrMin("mapName", 1) in saveSlotVer1ReadJSON.
- stringCopy(
- SAVE.slot.cachedData.mapName, "overworld",
- sizeof(SAVE.slot.cachedData.mapName)
- );
char_t *json;
size_t len;
@@ -178,7 +159,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
assert_true(stringEquals(read.cachedData.name, "Hero"));
assert_int_equal(read.cachedData.playerLevel, 42);
- assert_true(stringEquals(read.cachedData.mapName, "overworld"));
assert_true(read.cachedData.time.time == writtenTime.time);
// version/dataType are struct-only bookkeeping, never serialized - the
@@ -199,33 +179,18 @@ static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) {
// No "name" key (or an empty one) means a blank/never-used slot (see
// saveSlotInUse) - the reader returns right after reading that, without
- // needing or validating time/mapName/partyMembers/partyOrder at all, so
+ // needing or validating time at all, so
// this succeeds despite omitting fields that would otherwise be required.
errorret_t ret = slotFromJSON(&slot, "{\"version\":1}");
assert_true(errorIsOk(ret));
assert_int_equal(slot.cachedData.name[0], '\0');
- assert_int_equal(slot.cachedData.mapName[0], '\0');
// Not readInt32's own default of 1 - the early return happens before
// playerLevel is ever read, leaving the zero-init saveSlotCurrentReadJSON
// seeds ver1 with (see the memoryZero in saveSlotCurrentReadJSON).
assert_int_equal(slot.cachedData.playerLevel, 0);
}
-static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) {
- saveslot_t slot;
-
- // "name" is present (in use) but "mapName" is missing - a real save
- // (via saveSlotNewGame) always has both, so this is corrupt.
- errorret_t ret = slotFromJSON(
- &slot,
- "{\"version\":1,\"name\":\"Hero\","
- "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
- );
- assert_true(errorIsNotOk(ret));
- errorCatch(ret);
-}
-
static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
saveslot_t slot;
@@ -302,7 +267,6 @@ int main(void) {
cmocka_unit_test(test_saveSlotReadJSON_nullAsserts),
cmocka_unit_test(test_saveSlotReadJSON_roundTrip),
cmocka_unit_test(test_saveSlotReadJSON_blankSlot_shortCircuits),
- cmocka_unit_test(test_saveSlotReadJSON_inUseMissingMapName_errors),
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
@@ -310,5 +274,5 @@ int main(void) {
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
};
- return cmocka_run_group_tests(tests, assetGroupSetup, assetGroupTeardown);
+ return cmocka_run_group_tests(tests, NULL, NULL);
}