sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
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.gzRefractor Level
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; }