Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / abceb65

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz

Refractor Level

Christian Immanuel · 2026-06-03 18:20 · abceb65ab1f1fda3f7bbb32983d0b9b89b18c90c

 src/levels/goal.hpp                 |   2 +-
 src/levels/level.hpp                |  80 +++--
 src/levels/level/char_copy.cpp      | 184 ++++++++++++
 src/levels/level/sandbox.cpp        |  37 +++
 src/levels/level/tutorial.cpp       | 326 +++++++++++++++++++++
 src/levels/level_list.cpp           | 562 ------------------------------------
 src/levels/level_list.hpp           |   7 -
 src/states/game_state.cpp           |   7 +-
 src/states/menu_state.cpp           |   9 +-
 src/systems/level_runner_system.cpp |   4 +-
 src/systems/level_runner_system.hpp |   8 +-
 11 files changed, 612 insertions(+), 614 deletions(-)

diff --git a/src/levels/goal.hpp b/src/levels/goal.hpp
index f35d73e..1ff6b80 100644
--- a/src/levels/goal.hpp
+++ b/src/levels/goal.hpp
@@ -10,7 +10,7 @@
 // ─────────────────────────────────────────────────────────────────────────
 // Goal: declarative description of "what makes the current step complete".
 //
-// Each LevelStep owns one Goal. Each frame, the level runner builds a
+// Each Level::Step owns one Goal. Each frame, the level runner builds a
 // GoalContext from the live game state and asks goal_Satisfied(goal, ctx).
 // When that returns true (and the settle timer has elapsed), the step
 // advances.
diff --git a/src/levels/level.hpp b/src/levels/level.hpp
index 6e5a821..864a5d6 100644
--- a/src/levels/level.hpp
+++ b/src/levels/level.hpp
@@ -7,33 +7,53 @@
 #include "utils/globals.hpp"
 
 
-struct TokenGrant {
-    DynArray<std::string> choices;
-
-    static TokenGrant give(const std::string& text) {
-        return { { text } };
-    }
-    static TokenGrant choice(std::initializer_list<std::string> options) {
-        return { options };
-    }
-};
-
-
-struct LevelStep {
-    std::string              hint;
-    std::string              details;
-    DynArray<TokenGrant>  grants;
-    Scene                    scene;
-    bool                     reset_carry = false;
-    Goal                     goal = AllOf{};
-    int                      max_ai_words = 5;
-};
-
-
-struct Level {
-    std::string name;
-    std::string desc;
-    bool        dynamic_lib = false;
-    std::string source_file = {};
-    DynArray<LevelStep> steps;
-};
+namespace Level
+{
+
+
+    struct TokenGrant {
+        DynArray<std::string> choices;
+
+        static TokenGrant give(const std::string& text)
+                            { return { { text } }; }
+        static TokenGrant choice(std::initializer_list<std::string> options)
+                            { return { options }; }
+    };
+
+
+    struct Step {
+        std::string           hint;
+        std::string           details;
+        DynArray<TokenGrant>  grants;
+        Scene                 scene;
+        bool                  reset_carry  = false;
+        Goal                  goal         = AllOf{};
+        int                   max_ai_words = 5;
+    };
+
+
+    struct Data {
+        std::string name;
+        std::string desc;
+        bool        dynamic_lib = false;
+        std::string source_file = {};
+        DynArray<Step> steps;
+    };
+
+    extern Data build_Tutorial();
+    extern Data build_CharCopy();
+    extern Data build_Sandbox ();
+
+    inline const DynArray<Data> levels =
+    {
+        build_Tutorial(),
+        build_CharCopy(),
+        build_Sandbox (),
+    };
+
+    inline const Data& get_Level(size_t i)
+        { return levels[i]; }
+    inline size_t level_Count()
+        { return levels.size(); }
+
+}
diff --git a/src/levels/level/char_copy.cpp b/src/levels/level/char_copy.cpp
new file mode 100644
index 0000000..e164e33
--- /dev/null
+++ b/src/levels/level/char_copy.cpp
@@ -0,0 +1,184 @@
+#include "levels/level.hpp"
+#include "utils/config.hpp"
+
+
+Level::Data
+Level::build_CharCopy()
+{
+    return {
+        .name = "Char Copy",
+        .desc = "Implement a function that copies a char array and prints the result.",
+        .steps = {
+            // ── Step 0: main scaffold ─────────────────────────────────────
+            {
+                .hint    = "Build a working main()",
+                .details = R"(C programs start at main().
+
+main() returns an int and takes 'void' to declare no arguments.
+The body returns 0 to mean success.
+
+Goal:
+  int main(void) {
+      return 0;
+  }
+
+Grab tokens and arrange them on the editor wall.)",
+                .grants = {
+                    Level::TokenGrant::give("int"),
+                    Level::TokenGrant::give("main"),
+                    Level::TokenGrant::give("("),
+                    Level::TokenGrant::choice({ "void", "int", "char" }),
+                    Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give("{"),
+                    Level::TokenGrant::give("return"),
+                    Level::TokenGrant::give("0"),
+                    Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("}"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "int main ( void ) { return 0 ; }" },
+                .max_ai_words = 3,
+            },
+            // ── Step 1: copy_str signature ────────────────────────────────
+            {
+                .hint    = "Declare the copy_str function",
+                .details = R"(Now add a function above main().
+
+copy_str takes two char arrays — a source and a destination —
+and returns nothing (void).
+
+Note the brackets [ ] on each parameter — that marks them
+as arrays.
+
+Goal:
+  void copy_str(char src[], char dst[]) {
+  }
+  int main(void) { ... }
+
+Place this BEFORE main.)",
+                .grants = {
+                    Level::TokenGrant::give("void"),
+                    Level::TokenGrant::give("copy_str"),
+                    Level::TokenGrant::give("("),
+                    Level::TokenGrant::choice({ "char", "int", "float" }),
+                    Level::TokenGrant::give("src"),
+                    Level::TokenGrant::give("["),
+                    Level::TokenGrant::give("]"),
+                    Level::TokenGrant::give(","),
+                    Level::TokenGrant::give("char"),
+                    Level::TokenGrant::give("dst"),
+                    Level::TokenGrant::give("["),
+                    Level::TokenGrant::give("]"),
+                    Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give("{"),
+                    Level::TokenGrant::give("}"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { }" },
+                .max_ai_words = 3,
+            },
+            // ── Step 2: loop counter ──────────────────────────────────────
+            {
+                .hint    = "Add a loop counter inside copy_str",
+                .details = R"(Inside copy_str's body, declare an integer
+to track our position in the array.
+
+Goal (inside copy_str):
+  int i;
+
+You only need to add ONE line. Place it between the { and }
+of copy_str.)",
+                .grants = {
+                    Level::TokenGrant::choice({ "int", "float", "char" }),
+                    Level::TokenGrant::give("i"),
+                    Level::TokenGrant::give(";"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { int i ; }" },
+                .max_ai_words = 3,
+            },
+            // ── Step 3: for loop scaffold ─────────────────────────────────
+            {
+                .hint    = "Loop through src",
+                .details = R"(Add a for loop after 'int i;'.
+
+It starts at 0 and stops when i reaches the length of src.
+strlen() gives the length of a C string.
+
+Goal:
+  for (i = 0; i < strlen(src); i++) {
+  }
+
+This is the loop scaffold only — the body comes next.)",
+                .grants = {
+                    Level::TokenGrant::give("for"), Level::TokenGrant::give("("),
+                    Level::TokenGrant::give("i"), Level::TokenGrant::give("="), Level::TokenGrant::give("0"), Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("i"), Level::TokenGrant::give("<"), Level::TokenGrant::give("strlen"),
+                    Level::TokenGrant::give("("), Level::TokenGrant::give("src"), Level::TokenGrant::give(")"), Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("i"), Level::TokenGrant::give("++"), Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give("{"), Level::TokenGrant::give("}"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "for ( i = 0 ; i < strlen ( src ) ; i ++ ) { }" },
+                .max_ai_words = 3,
+            },
+            // ── Step 4: copy the character ────────────────────────────────
+            {
+                .hint    = "Copy one character per iteration",
+                .details = R"(Inside the for loop body, copy src[i] into dst[i].
+
+This single statement is the heart of the function — it
+copies the i-th character on each pass through the loop.
+
+Goal (inside the for loop):
+  dst[i] = src[i];)",
+                .grants = {
+                    Level::TokenGrant::give("dst"), Level::TokenGrant::give("["), Level::TokenGrant::give("i"), Level::TokenGrant::give("]"),
+                    Level::TokenGrant::give("="),
+                    Level::TokenGrant::give("src"), Level::TokenGrant::give("["), Level::TokenGrant::give("i"), Level::TokenGrant::give("]"),
+                    Level::TokenGrant::give(";"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "dst [ i ] = src [ i ] ;" },
+                .max_ai_words = 3,
+            },
+            // ── Step 5: call copy_str and print ──────────────────────────
+            {
+                .hint    = "Call copy_str and print the result",
+                .details = R"(In main(), do three things before 'return 0;':
+
+1. Declare a buffer:    char dst[100];
+2. Call copy_str:       copy_str("hello", dst);
+3. Print the result:    printf("%s", dst);
+
+The buffer holds the copied characters.
+copy_str fills it. printf displays it.
+
+Place all three lines BEFORE the existing return.)",
+                .grants = {
+                    Level::TokenGrant::give("char"), Level::TokenGrant::give("dst"), Level::TokenGrant::give("["),
+                    Level::TokenGrant::give("100"),  Level::TokenGrant::give("]"),   Level::TokenGrant::give(";"),
+
+                    Level::TokenGrant::give("copy_str"), Level::TokenGrant::give("("),
+                    Level::TokenGrant::give("\"hello\""),
+                    Level::TokenGrant::give(","), Level::TokenGrant::give("dst"),
+                    Level::TokenGrant::give(")"), Level::TokenGrant::give(";"),
+
+                    Level::TokenGrant::give("printf"), Level::TokenGrant::give("("),
+                    Level::TokenGrant::choice({ "\"%s\"", "\"%d\"", "\"%c\"" }),
+                    Level::TokenGrant::give(","), Level::TokenGrant::give("dst"),
+                    Level::TokenGrant::give(")"), Level::TokenGrant::give(";"),
+                },
+                .scene        = {},
+                .reset_carry  = false,
+                .goal         = CompilePattern{ "char dst [ 100 ] ; copy_str ( \"hello\" , dst ) ; printf ( \"%s\" , dst ) ;" },
+                .max_ai_words = 3,
+            },
+        },
+    };
+}
diff --git a/src/levels/level/sandbox.cpp b/src/levels/level/sandbox.cpp
new file mode 100644
index 0000000..15a65be
--- /dev/null
+++ b/src/levels/level/sandbox.cpp
@@ -0,0 +1,37 @@
+#include "levels/level.hpp"
+#include "utils/config.hpp"
+
+
+Level::Data
+Level::build_Sandbox()
+{
+    return {
+        .name = "Sandbox",
+        .desc = "Build the world yourself: compile a library that spawns objects.",
+        .dynamic_lib = true,
+        .source_file = "assets/sandbox.c",
+        .steps = {
+            {
+                .hint    = "Spawn a box with box(x,y,z, hx,hy,hz);",
+                .details = R"(This level IS the code you write.
+
+The editor wall is a C library. When you Compile, the game
+rebuilds it and spawns whatever your code describes.
+
+Two helpers are available:
+  box(x, y, z,  hx, hy, hz, dy); - a solid box at (x,y,z), half-size (hx,hy,hz), is_dynamic (dy)
+  text(x, y, z, "word");         - floating text at (x,y,z)
+
+Build something like:
+  box(0, 5, -3,  1, 1, 1, 0);
+
+Then press the Compile pult. Edit and recompile freely.)",
+                .grants = { },
+                .scene        = {},
+                .reset_carry  = true,
+                .goal         = AllOf{},
+                .max_ai_words = 0,
+            },
+        },
+    };
+}
diff --git a/src/levels/level/tutorial.cpp b/src/levels/level/tutorial.cpp
new file mode 100644
index 0000000..8c5c262
--- /dev/null
+++ b/src/levels/level/tutorial.cpp
@@ -0,0 +1,326 @@
+#include "levels/level.hpp"
+#include "utils/config.hpp"
+
+
+Level::Data
+Level::build_Tutorial()
+{
+    return {
+        .name = "Tutorial",
+        .desc = "Learn the controls and the code-building loop.",
+        .steps = {
+
+            {
+                .hint    = "Walk to the green plate",
+                .details = R"(Welcome.
+
+Use W, A, S, D to walk.
+Move the mouse to turn.
+
+A green plate sits on the floor a short walk away.
+Step onto it to continue.)",
+                .grants  = { },
+                .scene   = Scene{
+                    .words  = {},
+                    .plates = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale), 6.0 }, .tag = "" },
+                    },
+                    .platforms = {},
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = true,
+                .goal         = ReachPlate{ "" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Jump up the steps to the plate",
+                .details = R"(Press SPACE to jump.
+
+Ahead is a short staircase of blocks.
+Hop up them, one at a time, and stand on the
+plate at the top.
+
+From now on, plates that advance the tutorial sit
+up high — you'll need this jump to reach them.)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words = {},
+                    .plates = {
+                        { .pos = { 8.6, static_cast<double>(-ConfigGame::world_box_scale + 2.2f), 0.0 }, .tag = "" },
+                    },
+                    .platforms = {
+                        { .pos = { 5.0, static_cast<double>(-ConfigGame::world_box_scale + 0.4f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
+                        { .pos = { 6.8, static_cast<double>(-ConfigGame::world_box_scale + 1.1f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
+                        { .pos = { 8.6, static_cast<double>(-ConfigGame::world_box_scale + 1.8f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
+                    },
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = true,
+                .goal         = ReachPlate{ "" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Look at each wall, then jump onto the plate",
+                .details = R"(Move the mouse to look around.
+
+Four walls surround you:
+  - Editor:   where you build code (the pults face it)
+  - Compiler: shows compile messages
+  - Program:  shows your program's output
+  - Info:     this wall, with hints
+
+Tip: hold C to free-look without turning your body.
+
+When you've seen them all, jump onto the plate.)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words  = {},
+                    .plates = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), 6.0 }, .tag = "" },
+                    },
+                    .platforms = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), 6.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
+                    },
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = true,
+                .goal         = ReachPlate{ "" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Walk into the floating word and press E",
+                .details = R"(Words float in the air.
+
+Walk up to "hello" and press E to pick it up.
+Picked words trail in front of you as you move.)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words = {
+                        { .text = "hello", .pos = { 5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), 0.0 } },
+                    },
+                    .plates    = {},
+                    .platforms = {},
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = true,
+                .goal         = CollectWord{ "hello" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Pick up all three words",
+                .details = R"(You can carry several words at once.
+
+Collect "press", "any" and "key" with E.
+They queue up in the order you grab them.)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words = {
+                        { .text = "press", .pos = {  5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
+                        { .text = "any",   .pos = {  0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
+                        { .text = "key",   .pos = { -5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
+                    },
+                    .plates    = {},
+                    .platforms = {},
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = false,
+                .goal = AllOf{{
+                    CollectWord{ "press" },
+                    CollectWord{ "any"   },
+                    CollectWord{ "key"   },
+                }},
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Press Q to drop the last word",
+                .details = R"(Q drops the most recently picked word.
+
+The dropped word falls to the floor — handy for
+putting back something you grabbed by mistake.)",
+                .grants  = {},
+                .scene   = {},
+                .reset_carry  = false,
+                .goal         = DroppedWord{},
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "LEFT CLICK to throw a word",
+                .details = R"(Left click throws the word at the front of your line.
+
+Aim with the crosshair and throw — any direction is fine.
+
+Carrying several? Tap F to throw the WHOLE line at once.)",
+                .grants  = {},
+                .scene   = {},
+                .reset_carry  = false,
+                .goal         = ShotWord{},
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Grab the ammo, then shoot the red word",
+                .details = R"(Some words turn red and chase you to steal what
+you carry.
+
+Pick up "ammo", then knock out the red "danger"
+word by throwing at it with LEFT CLICK before it
+reaches you.)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words = {
+                        { .text = "ammo", .pos = { 6.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), 6.0 } },
+                    },
+                    .plates    = {},
+                    .platforms = {},
+                    .ai_seeds  = {
+                        {
+                            .text           = "danger",
+                            .pos            = { -6.0, static_cast<double>(-ConfigGame::world_box_scale + 2.0f), -6.0 },
+                            .initial_state  = Text3dAI::State::Attack,
+                            .aggressiveness = 1.0,
+                        },
+                    },
+                },
+                .reset_carry = true,
+                .goal = AllOf{{ CollectWord{ "ammo" }, AiWordDefeated{ "danger" } }},
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Build int main(void) { return 0; } and press BUILD",
+                .details = R"(Now make code.
+
+Face the Editor wall (the pults point at it). Throw
+tokens at the wall to place them:
+  - LEFT CLICK throws one word
+  - F throws your whole carried line at once
+
+Words snap into rows. Aim between two words to drop
+a token between them. Mis-placed a token? Walk up and
+press E to pluck it back off the wall.
+
+Build exactly:
+  int main(void) { return 0; }
+
+Then walk to the Compile pult (labelled BUILD) and
+press it.)",
+                .grants = {
+                    Level::TokenGrant::give("int"),
+                    Level::TokenGrant::give("main"),
+                    Level::TokenGrant::give("("),
+                    Level::TokenGrant::give("void"),
+                    Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give("{"),
+                    Level::TokenGrant::give("return"),
+                    Level::TokenGrant::give("0"),
+                    Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("}"),
+                },
+                .scene        = {},
+                .reset_carry  = true,
+                .goal         = CompilePattern{ "int main ( void ) { return 0 ; }" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Add printf, BUILD, then press RUN",
+                .details = R"(A program that does nothing is dull. Make it print.
+
+Add a printf line inside main, before return:
+  printf("hello");
+
+So the whole thing reads:
+  int main(void) { printf("hello"); return 0; }
+
+Press BUILD to compile, then walk to the RUN pult and
+press it. Your program runs and its output appears on
+the Program wall — and that's what finishes this step.)",
+                .grants = {
+                    Level::TokenGrant::give("int"),
+                    Level::TokenGrant::give("main"),
+                    Level::TokenGrant::give("("),
+                    Level::TokenGrant::give("void"),
+                    Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give("{"),
+                    Level::TokenGrant::give("printf"),
+                    Level::TokenGrant::give("("),
+                    Level::TokenGrant::give("\"hello\""),
+                    Level::TokenGrant::give(")"),
+                    Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("return"),
+                    Level::TokenGrant::give("0"),
+                    Level::TokenGrant::give(";"),
+                    Level::TokenGrant::give("}"),
+                },
+                .scene        = {},
+                .reset_carry  = true,
+                .goal = AllOf{{
+                    CompilePattern{ "int main ( void ) { printf ( \"hello\" ) ; return 0 ; }" },
+                    PressButton{ PultAction::RunProgram },
+                }},
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Try the editor pults, then jump onto the plate",
+                .details = R"(A few tools for managing the Editor wall:
+
+  - The UP / DOWN pults scroll the code.
+  - Each line is numbered on both sides. Shoot a line
+    number to scroll that line to your eye level.
+  - The CLEAR pult wipes the wall.
+  - Press E on a placed word to pull it back off.
+
+Have a play, then jump onto the plate to continue.)",
+                .grants  = {
+                    Level::TokenGrant::give("int"),
+                    Level::TokenGrant::give("x"),
+                    Level::TokenGrant::give("="),
+                    Level::TokenGrant::give("0"),
+                    Level::TokenGrant::give(";"),
+                },
+                .scene   = Scene{
+                    .words  = {},
+                    .plates = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), -6.0 }, .tag = "" },
+                    },
+                    .platforms = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), -6.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
+                    },
+                    .ai_seeds  = {},
+                },
+                .reset_carry  = true,
+                .goal         = ReachPlate{ "" },
+                .max_ai_words = 0,
+            },
+
+            {
+                .hint    = "Jump onto the plate to begin \"Char Copy\"",
+                .details = R"(You're ready.
+
+Jump onto the plate to start the first real level:
+"Char Copy".)",
+                .grants  = {},
+                .scene   = Scene{
+                    .words  = {},
+                    .plates = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), 0.0 }, .tag = "" },
+                    },
+                    .platforms = {
+                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), 0.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
+                    },
+                    .ai_seeds = {},
+                },
+                .reset_carry  = true,
+                .goal         = ReachPlate{ "" },
+                .max_ai_words = 0,
+            },
+        },
+    };
+}
diff --git a/src/levels/level_list.cpp b/src/levels/level_list.cpp
deleted file mode 100644
index e20696c..0000000
--- a/src/levels/level_list.cpp
+++ /dev/null
@@ -1,562 +0,0 @@
-#include "level_list.hpp"
-#include "utils/config.hpp"
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Layout conventions for the tutorial
-//
-//  * The player spawns near the world origin (0,0,0). Keep the FIRST thing a
-//    step asks for at least ~4 units away so nothing spawns under the player.
-//
-//  * "Continue plates" (ReachPlate goals that advance the tutorial) sit ON TOP
-//    of a solid box once jumping has been taught (step 1+). Elevating them means
-//    you can't drift onto them by accident — you have to deliberately jump up.
-//    The pattern is always: a box, and a plate centred on its top face.
-//
-//  * floor_y == -ConfigGame::world_box_scale. Heights are written as -ConfigGame::world_box_scale + dy.
-// ─────────────────────────────────────────────────────────────────────────────
-
-static const DynArray<Level> levels =
-{
-    {
-        .name = "Tutorial",
-        .desc = "Learn the controls and the code-building loop.",
-        .steps = {
-
-            // ── Step 0: walk to plate (floor — before jumping) ───────────────
-            {
-                .hint    = "Walk to the green plate",
-                .details = R"(Welcome.
-
-Use W, A, S, D to walk.
-Move the mouse to turn.
-
-A green plate sits on the floor a short walk away.
-Step onto it to continue.)",
-                .grants  = { },
-                .scene   = Scene{
-                    .words  = {},
-                    .plates = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale), 6.0 }, .tag = "" },
-                    },
-                    .platforms = {},
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = true,
-                .goal         = ReachPlate{ "" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 1: learn to jump (staircase to an elevated plate) ───────
-            {
-                .hint    = "Jump up the steps to the plate",
-                .details = R"(Press SPACE to jump.
-
-Ahead is a short staircase of blocks.
-Hop up them, one at a time, and stand on the
-plate at the top.
-
-From now on, plates that advance the tutorial sit
-up high — you'll need this jump to reach them.)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words = {},
-                    .plates = {
-                        { .pos = { 8.6, static_cast<double>(-ConfigGame::world_box_scale + 2.2f), 0.0 }, .tag = "" },
-                    },
-                    .platforms = {
-                        { .pos = { 5.0, static_cast<double>(-ConfigGame::world_box_scale + 0.4f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
-                        { .pos = { 6.8, static_cast<double>(-ConfigGame::world_box_scale + 1.1f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
-                        { .pos = { 8.6, static_cast<double>(-ConfigGame::world_box_scale + 1.8f), 0.0 }, .half_extents = { 0.9, 0.2, 0.9 } },
-                    },
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = true,
-                .goal         = ReachPlate{ "" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 2: look around (elevated continue-plate) ────────────────
-            {
-                .hint    = "Look at each wall, then jump onto the plate",
-                .details = R"(Move the mouse to look around.
-
-Four walls surround you:
-  - Editor:   where you build code (the pults face it)
-  - Compiler: shows compile messages
-  - Program:  shows your program's output
-  - Info:     this wall, with hints
-
-Tip: hold C to free-look without turning your body.
-
-When you've seen them all, jump onto the plate.)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words  = {},
-                    .plates = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), 6.0 }, .tag = "" },
-                    },
-                    .platforms = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), 6.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
-                    },
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = true,
-                .goal         = ReachPlate{ "" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 3: pick up a word ───────────────────────────────────────
-            {
-                .hint    = "Walk into the floating word and press E",
-                .details = R"(Words float in the air.
-
-Walk up to "hello" and press E to pick it up.
-Picked words trail in front of you as you move.)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words = {
-                        { .text = "hello", .pos = { 5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), 0.0 } },
-                    },
-                    .plates    = {},
-                    .platforms = {},
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = true,
-                .goal         = CollectWord{ "hello" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 4: pick up several words (order matters) ─────────────────
-            {
-                .hint    = "Pick up all three words",
-                .details = R"(You can carry several words at once.
-
-Collect "press", "any" and "key" with E.
-They queue up in the order you grab them.)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words = {
-                        { .text = "press", .pos = {  5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
-                        { .text = "any",   .pos = {  0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
-                        { .text = "key",   .pos = { -5.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), -3.0 } },
-                    },
-                    .plates    = {},
-                    .platforms = {},
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = false,
-                .goal = AllOf{{
-                    CollectWord{ "press" },
-                    CollectWord{ "any"   },
-                    CollectWord{ "key"   },
-                }},
-                .max_ai_words = 0,
-            },
-
-            // ── Step 5: drop a word ──────────────────────────────────────────
-            {
-                .hint    = "Press Q to drop the last word",
-                .details = R"(Q drops the most recently picked word.
-
-The dropped word falls to the floor — handy for
-putting back something you grabbed by mistake.)",
-                .grants  = {},
-                .scene   = {},
-                .reset_carry  = false,
-                .goal         = DroppedWord{},
-                .max_ai_words = 0,
-            },
-
-            // ── Step 6: throw a word ─────────────────────────────────────────
-            {
-                .hint    = "LEFT CLICK to throw a word",
-                .details = R"(Left click throws the word at the front of your line.
-
-Aim with the crosshair and throw — any direction is fine.
-
-Carrying several? Tap F to throw the WHOLE line at once.)",
-                .grants  = {},
-                .scene   = {},
-                .reset_carry  = false,
-                .goal         = ShotWord{},
-                .max_ai_words = 0,
-            },
-
-            // ── Step 7: combat — shoot a hostile word ────────────────────────
-            {
-                .hint    = "Grab the ammo, then shoot the red word",
-                .details = R"(Some words turn red and chase you to steal what
-you carry.
-
-Pick up "ammo", then knock out the red "danger"
-word by throwing at it with LEFT CLICK before it
-reaches you.)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words = {
-                        { .text = "ammo", .pos = { 6.0, static_cast<double>(-ConfigGame::world_box_scale + 1.5f), 6.0 } },
-                    },
-                    .plates    = {},
-                    .platforms = {},
-                    .ai_seeds  = {
-                        {
-                            .text           = "danger",
-                            .pos            = { -6.0, static_cast<double>(-ConfigGame::world_box_scale + 2.0f), -6.0 },
-                            .initial_state  = Text3dAI::State::Attack,
-                            .aggressiveness = 1.0,
-                        },
-                    },
-                },
-                .reset_carry = true,
-                .goal = AllOf{{ CollectWord{ "ammo" }, AiWordDefeated{ "danger" } }},
-                .max_ai_words = 0,
-            },
-
-            // ── Step 8: build on the editor wall + compile ───────────────────
-            {
-                .hint    = "Build int main(void) { return 0; } and press BUILD",
-                .details = R"(Now make code.
-
-Face the Editor wall (the pults point at it). Throw
-tokens at the wall to place them:
-  - LEFT CLICK throws one word
-  - F throws your whole carried line at once
-
-Words snap into rows. Aim between two words to drop
-a token between them. Mis-placed a token? Walk up and
-press E to pluck it back off the wall.
-
-Build exactly:
-  int main(void) { return 0; }
-
-Then walk to the Compile pult (labelled BUILD) and
-press it.)",
-                .grants = {
-                    TokenGrant::give("int"),
-                    TokenGrant::give("main"),
-                    TokenGrant::give("("),
-                    TokenGrant::give("void"),
-                    TokenGrant::give(")"),
-                    TokenGrant::give("{"),
-                    TokenGrant::give("return"),
-                    TokenGrant::give("0"),
-                    TokenGrant::give(";"),
-                    TokenGrant::give("}"),
-                },
-                .scene        = {},
-                .reset_carry  = true,
-                .goal         = CompilePattern{ "int main ( void ) { return 0 ; }" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 9: make it print, then RUN it ───────────────────────────
-            {
-                .hint    = "Add printf, BUILD, then press RUN",
-                .details = R"(A program that does nothing is dull. Make it print.
-
-Add a printf line inside main, before return:
-  printf("hello");
-
-So the whole thing reads:
-  int main(void) { printf("hello"); return 0; }
-
-Press BUILD to compile, then walk to the RUN pult and
-press it. Your program runs and its output appears on
-the Program wall — and that's what finishes this step.)",
-                .grants = {
-                    TokenGrant::give("int"),
-                    TokenGrant::give("main"),
-                    TokenGrant::give("("),
-                    TokenGrant::give("void"),
-                    TokenGrant::give(")"),
-                    TokenGrant::give("{"),
-                    TokenGrant::give("printf"),
-                    TokenGrant::give("("),
-                    TokenGrant::give("\"hello\""),
-                    TokenGrant::give(")"),
-                    TokenGrant::give(";"),
-                    TokenGrant::give("return"),
-                    TokenGrant::give("0"),
-                    TokenGrant::give(";"),
-                    TokenGrant::give("}"),
-                    TokenGrant::give("#include"),
-                    TokenGrant::give("<stdio.h>"),
-                },
-                .scene        = {},
-                .reset_carry  = true,
-                // Must build the printf program (compiles + matches) AND press RUN.
-                .goal = AllOf{{
-                    CompilePattern{ "int main ( void ) { printf ( \"hello\" ) ; return 0 ; }" },
-                    PressButton{ PultAction::RunProgram },
-                }},
-                .max_ai_words = 0,
-            },
-
-            // ── Step 10: editor tools (scroll / clear / line numbers) ────────
-            {
-                .hint    = "Try the editor pults, then jump onto the plate",
-                .details = R"(A few tools for managing the Editor wall:
-
-  - The UP / DOWN pults scroll the code.
-  - Each line is numbered on both sides. Shoot a line
-    number to scroll that line to your eye level.
-  - The CLEAR pult wipes the wall.
-  - Press E on a placed word to pull it back off.
-
-Have a play, then jump onto the plate to continue.)",
-                .grants  = {
-                    TokenGrant::give("int"),
-                    TokenGrant::give("x"),
-                    TokenGrant::give("="),
-                    TokenGrant::give("0"),
-                    TokenGrant::give(";"),
-                },
-                .scene   = Scene{
-                    .words  = {},
-                    .plates = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), -6.0 }, .tag = "" },
-                    },
-                    .platforms = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), -6.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
-                    },
-                    .ai_seeds  = {},
-                },
-                .reset_carry  = true,
-                .goal         = ReachPlate{ "" },
-                .max_ai_words = 0,
-            },
-
-            // ── Step 11: finish (elevated plate → first real level) ──────────
-            {
-                .hint    = "Jump onto the plate to begin \"Char Copy\"",
-                .details = R"(You're ready.
-
-Jump onto the plate to start the first real level:
-"Char Copy".)",
-                .grants  = {},
-                .scene   = Scene{
-                    .words  = {},
-                    .plates = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 1.2f), 0.0 }, .tag = "" },
-                    },
-                    .platforms = {
-                        { .pos = { 0.0, static_cast<double>(-ConfigGame::world_box_scale + 0.6f), 0.0 }, .half_extents = { 1.0, 0.4, 1.0 } },
-                    },
-                    .ai_seeds = {},
-                },
-                .reset_carry  = true,
-                .goal         = ReachPlate{ "" },
-                .max_ai_words = 0,
-            },
-        },
-    },
-
-    {
-        .name = "Char Copy",
-        .desc = "Implement a function that copies a char array and prints the result.",
-        .steps = {
-            // ── Step 0: main scaffold ─────────────────────────────────────
-            {
-                .hint    = "Build a working main()",
-                .details = R"(C programs start at main().
-
-main() returns an int and takes 'void' to declare no arguments.
-The body returns 0 to mean success.
-
-Goal:
-  int main(void) {
-      return 0;
-  }
-
-Grab tokens and arrange them on the editor wall.)",
-                .grants = {
-                    TokenGrant::give("int"),
-                    TokenGrant::give("main"),
-                    TokenGrant::give("("),
-                    TokenGrant::choice({ "void", "int", "char" }),
-                    TokenGrant::give(")"),
-                    TokenGrant::give("{"),
-                    TokenGrant::give("return"),
-                    TokenGrant::give("0"),
-                    TokenGrant::give(";"),
-                    TokenGrant::give("}"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "int main ( void ) { return 0 ; }" },
-                .max_ai_words = 3,
-            },
-            // ── Step 1: copy_str signature ────────────────────────────────
-            {
-                .hint    = "Declare the copy_str function",
-                .details = R"(Now add a function above main().
-
-copy_str takes two char arrays — a source and a destination —
-and returns nothing (void).
-
-Note the brackets [ ] on each parameter — that marks them
-as arrays.
-
-Goal:
-  void copy_str(char src[], char dst[]) {
-  }
-  int main(void) { ... }
-
-Place this BEFORE main.)",
-                .grants = {
-                    TokenGrant::give("void"),
-                    TokenGrant::give("copy_str"),
-                    TokenGrant::give("("),
-                    TokenGrant::choice({ "char", "int", "float" }),
-                    TokenGrant::give("src"),
-                    TokenGrant::give("["),
-                    TokenGrant::give("]"),
-                    TokenGrant::give(","),
-                    TokenGrant::give("char"),
-                    TokenGrant::give("dst"),
-                    TokenGrant::give("["),
-                    TokenGrant::give("]"),
-                    TokenGrant::give(")"),
-                    TokenGrant::give("{"),
-                    TokenGrant::give("}"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { }" },
-                .max_ai_words = 3,
-            },
-            // ── Step 2: loop counter ──────────────────────────────────────
-            {
-                .hint    = "Add a loop counter inside copy_str",
-                .details = R"(Inside copy_str's body, declare an integer
-to track our position in the array.
-
-Goal (inside copy_str):
-  int i;
-
-You only need to add ONE line. Place it between the { and }
-of copy_str.)",
-                .grants = {
-                    TokenGrant::choice({ "int", "float", "char" }),
-                    TokenGrant::give("i"),
-                    TokenGrant::give(";"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "void copy_str ( char src [ ] , char dst [ ] ) { int i ; }" },
-                .max_ai_words = 3,
-            },
-            // ── Step 3: for loop scaffold ─────────────────────────────────
-            {
-                .hint    = "Loop through src",
-                .details = R"(Add a for loop after 'int i;'.
-
-It starts at 0 and stops when i reaches the length of src.
-strlen() gives the length of a C string.
-
-Goal:
-  for (i = 0; i < strlen(src); i++) {
-  }
-
-This is the loop scaffold only — the body comes next.)",
-                .grants = {
-                    TokenGrant::give("for"), TokenGrant::give("("),
-                    TokenGrant::give("i"), TokenGrant::give("="), TokenGrant::give("0"), TokenGrant::give(";"),
-                    TokenGrant::give("i"), TokenGrant::give("<"), TokenGrant::give("strlen"),
-                    TokenGrant::give("("), TokenGrant::give("src"), TokenGrant::give(")"), TokenGrant::give(";"),
-                    TokenGrant::give("i"), TokenGrant::give("++"), TokenGrant::give(")"),
-                    TokenGrant::give("{"), TokenGrant::give("}"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "for ( i = 0 ; i < strlen ( src ) ; i ++ ) { }" },
-                .max_ai_words = 3,
-            },
-            // ── Step 4: copy the character ────────────────────────────────
-            {
-                .hint    = "Copy one character per iteration",
-                .details = R"(Inside the for loop body, copy src[i] into dst[i].
-
-This single statement is the heart of the function — it
-copies the i-th character on each pass through the loop.
-
-Goal (inside the for loop):
-  dst[i] = src[i];)",
-                .grants = {
-                    TokenGrant::give("dst"), TokenGrant::give("["), TokenGrant::give("i"), TokenGrant::give("]"),
-                    TokenGrant::give("="),
-                    TokenGrant::give("src"), TokenGrant::give("["), TokenGrant::give("i"), TokenGrant::give("]"),
-                    TokenGrant::give(";"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "dst [ i ] = src [ i ] ;" },
-                .max_ai_words = 3,
-            },
-            // ── Step 5: call copy_str and print ──────────────────────────
-            {
-                .hint    = "Call copy_str and print the result",
-                .details = R"(In main(), do three things before 'return 0;':
-
-1. Declare a buffer:    char dst[100];
-2. Call copy_str:       copy_str("hello", dst);
-3. Print the result:    printf("%s", dst);
-
-The buffer holds the copied characters.
-copy_str fills it. printf displays it.
-
-Place all three lines BEFORE the existing return.)",
-                .grants = {
-                    TokenGrant::give("char"), TokenGrant::give("dst"), TokenGrant::give("["),
-                    TokenGrant::give("100"),  TokenGrant::give("]"),   TokenGrant::give(";"),
-
-                    TokenGrant::give("copy_str"), TokenGrant::give("("),
-                    TokenGrant::give("\"hello\""),
-                    TokenGrant::give(","), TokenGrant::give("dst"),
-                    TokenGrant::give(")"), TokenGrant::give(";"),
-
-                    TokenGrant::give("printf"), TokenGrant::give("("),
-                    TokenGrant::choice({ "\"%s\"", "\"%d\"", "\"%c\"" }),
-                    TokenGrant::give(","), TokenGrant::give("dst"),
-                    TokenGrant::give(")"), TokenGrant::give(";"),
-                },
-                .scene        = {},
-                .reset_carry  = false,
-                .goal         = CompilePattern{ "char dst [ 100 ] ; copy_str ( \"hello\" , dst ) ; printf ( \"%s\" , dst ) ;" },
-                .max_ai_words = 3,
-            },
-        },
-    },
-    {
-        .name = "Sandbox",
-        .desc = "Build the world yourself: compile a library that spawns objects.",
-        .dynamic_lib = true,
-        .source_file = "assets/sandbox.c",
-        .steps = {
-            {
-                .hint    = "Spawn a box with box(x,y,z, hx,hy,hz);",
-                .details = R"(This level IS the code you write.
-
-The editor wall is a C library. When you Compile, the game
-rebuilds it and spawns whatever your code describes.
-
-Two helpers are available:
-  box(x, y, z,  hx, hy, hz, dy); - a solid box at (x,y,z), half-size (hx,hy,hz), is_dynamic (dy)
-  text(x, y, z, "word");         - floating text at (x,y,z)
-
-Build something like:
-  box(0, 5, -3,  1, 1, 1, 0);
-
-Then press the Compile pult. Edit and recompile freely.)",
-                .grants = { },
-                .scene        = {},
-                .reset_carry  = true,
-                .goal         = AllOf{},
-                .max_ai_words = 0,
-            },
-        },
-    },
-};
-
-const Level& get_Level(size_t i) { return levels[i]; }
-size_t       level_Count()       { return levels.size(); }
diff --git a/src/levels/level_list.hpp b/src/levels/level_list.hpp
deleted file mode 100644
index cfdc87c..0000000
--- a/src/levels/level_list.hpp
+++ /dev/null
@@ -1,7 +0,0 @@
-#pragma once
-
-#include "level.hpp"
-#include "utils/globals.hpp"
-
-const Level& get_Level(size_t index);
-size_t       level_Count();
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index 4e7ca91..cbd7741 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -9,7 +9,6 @@
 #include "entities/world.hpp"
 #include "game_state.hpp"
 #include "levels/level_check.hpp"
-#include "levels/level_list.hpp"
 #include "rendering/gl_renderer.hpp"
 #include "systems/celebration_system.hpp"
 #include "systems/game_mod_abi.h"
@@ -53,8 +52,8 @@ GameState::GameState(size_t starting_level)
     _pult_sys        = cpputils::make_unique<PultSystem>  ();
     _pult_sys->init(_ctx.scenery, _ctx.buttons, _ctx.world->scale());
 
-    if (starting_level >= level_Count()) starting_level = 0;
-    _level_runner_sys = cpputils::make_unique<LevelRunner>(get_Level(starting_level));
+    if (starting_level >= Level::level_Count()) starting_level = 0;
+    _level_runner_sys = cpputils::make_unique<LevelRunner>(Level::get_Level(starting_level));
 
     DynArray<std::string> tokens = _level_runner_sys->grants_for_step();
     step_Spawn(tokens);
@@ -783,7 +782,7 @@ GameState::step_SettingsApply()
         return;
     }
 
-    const LevelStep& step = _level_runner_sys->current();
+    const Level::Step& step = _level_runner_sys->current();
     _word_ai_sys->set_MaxActiveAi(step.max_ai_words);
 }
 
diff --git a/src/states/menu_state.cpp b/src/states/menu_state.cpp
index 0bcad66..24c61ad 100644
--- a/src/states/menu_state.cpp
+++ b/src/states/menu_state.cpp
@@ -4,7 +4,7 @@
 #include <cmath>
 
 #include "core/game.hpp"
-#include "levels/level_list.hpp"
+#include "levels/level.hpp"
 #include "rendering/gl_renderer.hpp"
 #include "states/game_state.hpp"
 
@@ -113,9 +113,9 @@ MenuState::build_Items()
     // We DON'T set slot_pos.y here; vertical position is recomputed each
     // frame in update() based on _scroll_current. The X/Z components (the
     // middle column) are fixed.
-    for (size_t i = 0; i < level_Count(); ++i) {
+    for (size_t i = 0; i < Level::level_Count(); ++i) {
         MenuItem mi;
-        mi.label       = get_Level(i).name;
+        mi.label       = Level::get_Level(i).name;
         mi.kind        = ItemKind::StartLevel;
         mi.level_index = i;
         mi.slot_pos    = slot_PosForColumn(COLUMN_MID_RAD, EYE_Y);   // y replaced each frame
@@ -192,7 +192,8 @@ MenuState::handle_Event(Game& game, const SDL_Event& e)
         case SDLK_DOWN:
             if (is_LevelItem(pick_FromCrosshair())) {
                 const double max_scroll =
-                    static_cast<double>(level_Count() > 0 ? level_Count() - 1 : 0);
+                    static_cast<double>(Level::level_Count() > 0
+                                        ? Level::level_Count() - 1 : 0);
                 if (_scroll_target < max_scroll) _scroll_target += 1.0;
             }
             break;
diff --git a/src/systems/level_runner_system.cpp b/src/systems/level_runner_system.cpp
index 400468e..2167b83 100644
--- a/src/systems/level_runner_system.cpp
+++ b/src/systems/level_runner_system.cpp
@@ -1,6 +1,6 @@
 #include "level_runner_system.hpp"
 
-LevelRunner::LevelRunner(const Level& l) : _level(&l) {}
+LevelRunner::LevelRunner(const Level::Data& l) : _level(&l) {}
 
 bool LevelRunner::advance()
 {
@@ -11,7 +11,7 @@ bool LevelRunner::advance()
 DynArray<std::string> LevelRunner::grants_for_step() const
 {
     DynArray<std::string> result;
-    for (const TokenGrant& grant : current().grants)
+    for (const Level::TokenGrant& grant : current().grants)
         for (const std::string& c : grant.choices)
             result.push_back(c);
     return result;
diff --git a/src/systems/level_runner_system.hpp b/src/systems/level_runner_system.hpp
index 9936b83..15aea80 100644
--- a/src/systems/level_runner_system.hpp
+++ b/src/systems/level_runner_system.hpp
@@ -2,10 +2,10 @@
 #include "levels/level.hpp"
 
 class LevelRunner {
-    const Level* _level;
-    size_t       _current_step{0};
+    const Level::Data* _level;
+    size_t           _current_step{0};
 public:
-    explicit LevelRunner(const Level& _level);
+    explicit LevelRunner(const Level::Data& _level);
 
     // Call after a successful compile
     // Returns true if there are more steps, false if _level complete
@@ -13,7 +13,7 @@ public:
 
     bool               complete()    const { return _current_step >= _level->steps.size(); }
     size_t             step()        const { return _current_step; }
-    const LevelStep&   current()     const { return _level->steps[_current_step]; }
+    const Level::Step&   current()     const { return _level->steps[_current_step]; }
     bool               dynamic_lib() const { return _level->dynamic_lib; }
     const std::string& source_file() const { return _level->source_file; }