From 521b5da83485a956811fc510cba9f139acad8b26 Mon Sep 17 00:00:00 2001 From: "Emil A. Overbeck" Date: Sun, 19 Jul 2026 04:19:48 +0200 Subject: New maps and map selector --- src/engine/app.cpp | 65 +++++++++++++++++++++++++------- src/game/data.cpp | 107 ++++++++++++++++++++++++++++++++++++++--------------- src/game/data.hpp | 37 +++++++++++++----- 3 files changed, 156 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/engine/app.cpp b/src/engine/app.cpp index fa1155c..0de71c5 100644 --- a/src/engine/app.cpp +++ b/src/engine/app.cpp @@ -89,7 +89,9 @@ void App::run() { camera.set_zoom(10.0f); // The world is a simple half-plane: below ground_level is solid ground, - // above is sky. Anything that falls into the ground is destroyed. + // above is sky. Anything that falls into the ground is destroyed. The + // selected map sets the actual height (build_map); this is just the default + // until the first map is loaded below. graph.ground_level = 1.0f; // Resolve each material's texture once: prefer BYO Forts art, fall back to @@ -137,7 +139,9 @@ void App::run() { // --- Devices + map (M5) --------------------------------------------------- std::vector device_defs = load_devices(script, "data"); - MapData map = load_map(script, "data"); + std::vector maps = load_maps(script, "data"); + if (maps.empty()) maps.push_back(MapDef{}); // empty fallback: flat, bare + int current_map = 0; // Maps[1] in Lua = the default std::vector dev_tex(device_defs.size(), nullptr); for (size_t i = 0; i < device_defs.size(); i++) dev_tex[i] = renderer.load_texture_or(device_defs[i].texture_forts, @@ -155,7 +159,7 @@ void App::run() { int turbine_i = devidx("turbine"), mine_i = devidx("mine"), battery_i = devidx("battery"); auto on_deposit = [&](float x, float y) -> bool { - for (auto& d : map.deposits) + for (auto& d : maps[current_map].deposits) if (std::hypot(x - d.first, y - d.second) < 1.6f) return true; return false; }; @@ -164,22 +168,22 @@ void App::run() { float metal = 0.0f, energy = 0.0f, ecap = BASE_ECAP, mcap = BASE_MCAP; bool game_over = false; int winner = -1; // 0 = player, 1 = enemy - // Build the scenario once, snapshot the graph, then reset() rebuilds it all - // (used on 'R') WITHOUT re-running the Lua (which would duplicate structures). - load_structures(script, "data", graph); - std::vector snap_nodes = graph.nodes; - std::vector snap_edges = graph.edges; + // Snapshot of the current map's prebuilt structure. reset() ('R') restores + // from it rather than re-running the Lua, which would duplicate the forts. + std::vector snap_nodes; + std::vector snap_edges; int selected_cannon = -1; int cur_mat = 0; int place_dev = -1; // >=0 = placing that device type; -1 = build materials auto reset = [&]() { + const MapDef& m = maps[current_map]; graph.nodes = snap_nodes; graph.edges = snap_edges; graph.rebuild(); devices.clear(); - for (auto& pd : map.devices) { + for (auto& pd : m.devices) { int di = -1; for (size_t i = 0; i < device_defs.size(); i++) if (device_defs[i].name == pd.type) di = (int)i; @@ -189,14 +193,29 @@ void App::run() { devices.push_back(dv); } cannons.clear(); - cannons.push_back({-18.0f, graph.ground_level + 0.6f, 0.0f, 0.0f, 0.0f, cannon_type}); - if (laser_type >= 0) - cannons.push_back({-14.0f, graph.ground_level + 0.6f, 0.0f, 0.0f, 0.0f, laser_type}); + for (auto& pw : m.weapons) { + int t = pw.type == "laser" ? laser_type : cannon_type; + if (t < 0) continue; + cannons.push_back({pw.x, pw.y, 0.0f, 0.0f, 0.0f, t}); + } + if (cannons.empty()) // map declared no weapons: fall back to a cannon + cannons.push_back({-18.0f, graph.ground_level + 0.6f, + 0.0f, 0.0f, 0.0f, cannon_type}); projectiles.clear(); beams.clear(); explosions.clear(); metal = 200.0f; energy = 200.0f; game_over = false; winner = -1; selected_cannon = -1; place_dev = -1; }; - reset(); + + // Tear the world down and rebuild it from maps[i]. Used at startup and by + // the Dev panel's map selector. + auto load_map_at = [&](int i) { + current_map = std::clamp(i, 0, (int)maps.size() - 1); + build_map(maps[current_map], graph); // also sets graph.ground_level + snap_nodes = graph.nodes; + snap_edges = graph.edges; + reset(); + }; + load_map_at(current_map); // Build state int drag_node = -1; @@ -517,7 +536,7 @@ void App::run() { } // Metal deposits (markers on the ground; mines must sit on these). - for (auto& d : map.deposits) { + for (auto& d : maps[current_map].deposits) { if (deposit_tex) renderer.draw_sprite(d.first, d.second + 0.2f, 0.35f, 0.35f, 0.0f, deposit_tex); else renderer.draw_box(d.first, d.second + 0.2f, 0.3f, 0.3f, 0.0f, 0.8f, 0.7f, 0.2f, 0.9f); } @@ -788,6 +807,24 @@ void App::run() { if (battery_i >= 0) { ImGui::SameLine(); if (ImGui::Button("battery")) { place_dev = battery_i; selected_cannon = -1; } } if (ImGui::Button("restart (R)")) reset(); ImGui::Separator(); + // Map selector. Picking one rebuilds the world from that map. + if (ImGui::CollapsingHeader("Map", ImGuiTreeNodeFlags_DefaultOpen)) { + const MapDef& cur = maps[current_map]; + if (ImGui::BeginCombo("scenario", cur.name.c_str())) { + for (int i = 0; i < (int)maps.size(); i++) { + bool sel = (i == current_map); + if (ImGui::Selectable(maps[i].name.c_str(), sel) && !sel) + load_map_at(i); + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (!cur.description.empty()) + ImGui::TextWrapped("%s", cur.description.c_str()); + ImGui::Text("ground %.1f deposits %zu struts %zu", + cur.ground_level, cur.deposits.size(), graph.edges.size()); + if (ImGui::Button("reload map")) load_map_at(current_map); + } if (ImGui::CollapsingHeader("Physics", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("gravity", &graph.gravity, 0.0f, 40.0f); ImGui::SliderFloat("air drag", &graph.air_drag, 0.0f, 3.0f); diff --git a/src/game/data.cpp b/src/game/data.cpp index 3d9f6c0..bbcf2d0 100644 --- a/src/game/data.cpp +++ b/src/game/data.cpp @@ -163,11 +163,17 @@ std::vector load_weapons(ScriptEngine& script, const std::string& dat return out; } -void load_structures(ScriptEngine& script, const std::string& data_dir, - BuildGraph& graph) { - run_layered(script, data_dir, "structures.lua"); - sol::optional structs = script.lua()["Structures"]; - if (!structs) return; +void build_map(const MapDef& m, BuildGraph& graph) { + graph.nodes.clear(); + graph.edges.clear(); + // Clearing the vectors does NOT touch the private adjacency and edge-key + // caches, so rebuild them now. Skipping this leaves the PREVIOUS map's edge + // keys in the set and add_edge then refuses the new map's beams as + // duplicates, quietly producing a fort that collapses on load. + graph.rebuild(); + // Set the ground BEFORE welding: node_at asks is_on_ground() to decide which + // endpoints are pinned foundations. + graph.ground_level = m.ground_level; auto mat_index = [&](const std::string& nm) -> int { for (int i = 0; i < (int)graph.materials.size(); i++) @@ -180,25 +186,22 @@ void load_structures(ScriptEngine& script, const std::string& data_dir, }; int built = 0; - for (std::size_t si = 1; ; si++) { - sol::object so = (*structs)[si]; - if (so.get_type() != sol::type::table) break; - sol::table S = so.as(); - sol::optional beams = S["beams"]; - if (!beams) continue; - for (std::size_t bi = 1; ; bi++) { - sol::object bo = (*beams)[bi]; - if (bo.get_type() != sol::type::table) break; - sol::table b = bo.as(); - float x1 = b.get_or(1, 0.0f), y1 = b.get_or(2, 0.0f); - float x2 = b.get_or(3, 0.0f), y2 = b.get_or(4, 0.0f); - std::string mat = b.get_or(5, std::string("wood")); - if (graph.add_edge(node_at(x1, y1), node_at(x2, y2), mat_index(mat)) >= 0) - built++; - } + for (const auto& b : m.beams) { + int na = node_at(b.x1, b.y1), nb = node_at(b.x2, b.y2); + if (graph.add_edge(na, nb, mat_index(b.material)) >= 0) { built++; continue; } + // A dropped beam silently weakens the fort, so never fail quietly: + // endpoints closer than SNAP_RADIUS collapse onto one node, and a + // repeated node pair is refused as a duplicate. + fprintf(stderr, + "map '%s': beam (%g,%g)-(%g,%g) [%s] rejected -> nodes %d,%d%s\n", + m.name.c_str(), b.x1, b.y1, b.x2, b.y2, b.material.c_str(), + na, nb, na == nb ? " (endpoints snapped together)" : " (duplicate)"); } + for (auto& e : graph.edges) e.settle = 0.0f; // prebuilt = already cured - printf("Loaded %d prebuilt struts from Structures\n", built); + graph.rebuild(); + printf("Map '%s': %d prebuilt struts, ground %.1f\n", + m.name.c_str(), built, m.ground_level); } namespace { @@ -243,10 +246,14 @@ std::vector load_devices(ScriptEngine& script, const std::string& dat return out; } -MapData load_map(ScriptEngine& script, const std::string& data_dir) { - MapData m; - run_layered(script, data_dir, "map.lua"); - if (sol::optional dep = script.lua()["Deposits"]) { +namespace { +MapDef read_map(const sol::table& M) { + MapDef m; + m.name = M.get_or("name", std::string("unnamed")); + m.description = M.get_or("description", std::string("")); + m.ground_level = M.get_or("ground_level", 1.0f); + + if (sol::optional dep = M["deposits"]) { for (std::size_t i = 1; ; i++) { sol::object o = (*dep)[i]; if (o.get_type() != sol::type::table) break; @@ -254,9 +261,9 @@ MapData load_map(ScriptEngine& script, const std::string& data_dir) { m.deposits.emplace_back(p.get_or(1, 0.0f), p.get_or(2, 0.0f)); } } - if (sol::optional md = script.lua()["MapDevices"]) { + if (sol::optional dv = M["devices"]) { for (std::size_t i = 1; ; i++) { - sol::object o = (*md)[i]; + sol::object o = (*dv)[i]; if (o.get_type() != sol::type::table) break; sol::table d = o.as(); m.devices.push_back({ d.get_or("type", std::string("reactor")), @@ -264,6 +271,48 @@ MapData load_map(ScriptEngine& script, const std::string& data_dir) { d.get_or("team", 0) }); } } - printf("Loaded map: %zu deposits, %zu devices\n", m.deposits.size(), m.devices.size()); + if (sol::optional wp = M["weapons"]) { + for (std::size_t i = 1; ; i++) { + sol::object o = (*wp)[i]; + if (o.get_type() != sol::type::table) break; + sol::table w = o.as(); + m.weapons.push_back({ w.get_or("type", std::string("cannon")), + w.get_or("x", 0.0f), w.get_or("y", 0.0f) }); + } + } + // structures[] -> beams[] {x1,y1,x2,y2,material}, flattened: which fort a + // beam belonged to does not matter once it is welded into the graph. + if (sol::optional st = M["structures"]) { + for (std::size_t si = 1; ; si++) { + sol::object so = (*st)[si]; + if (so.get_type() != sol::type::table) break; + sol::table S = so.as(); + sol::optional beams = S["beams"]; + if (!beams) continue; + for (std::size_t bi = 1; ; bi++) { + sol::object bo = (*beams)[bi]; + if (bo.get_type() != sol::type::table) break; + sol::table b = bo.as(); + m.beams.push_back({ b.get_or(1, 0.0f), b.get_or(2, 0.0f), + b.get_or(3, 0.0f), b.get_or(4, 0.0f), + b.get_or(5, std::string("wood")) }); + } + } + } return m; } +} // namespace + +std::vector load_maps(ScriptEngine& script, const std::string& data_dir) { + std::vector out; + run_layered(script, data_dir, "maps.lua"); + sol::optional maps = script.lua()["Maps"]; + if (!maps) { fprintf(stderr, "load_maps: no global 'Maps' table\n"); return out; } + for (std::size_t i = 1; ; i++) { + sol::object o = (*maps)[i]; + if (o.get_type() != sol::type::table) break; + out.push_back(read_map(o.as())); + } + printf("Loaded %zu maps\n", out.size()); + return out; +} diff --git a/src/game/data.hpp b/src/game/data.hpp index 9b9aec1..3207ba0 100644 --- a/src/game/data.hpp +++ b/src/game/data.hpp @@ -43,11 +43,9 @@ std::vector load_materials(ScriptEngine& script, std::vector load_weapons(ScriptEngine& script, const std::string& data_dir); -// Loads prebuilt structures from /structures.lua (+ mods) and builds -// them into `graph`. Each structure is a list of beams {x1,y1,x2,y2,material}. -// Used for scenario forts (e.g. the enemy target). Requires graph.materials set. -void load_structures(ScriptEngine& script, const std::string& data_dir, - BuildGraph& graph); +// A prebuilt strut of a scenario fort, kept in map data so a map can be rebuilt +// on demand (the dev UI switches maps at runtime). +struct MapBeam { float x1, y1, x2, y2; std::string material; }; // A device type (reactor / mine / turbine / battery / ...), from data/devices.lua. struct DeviceDef { @@ -66,13 +64,32 @@ struct DeviceDef { float r, g, b; // fallback colour when no texture }; -// Scenario map: metal deposits + pre-placed devices (player reactor, enemy -// reactor, starter generators), from data/map.lua (+ mods). struct PlacedDevice { std::string type; float x, y; int team; }; -struct MapData { +struct PlacedWeapon { std::string type; float x, y; }; + +// One selectable scenario from data/maps.lua (+ mod layers): the ground height, +// metal deposits, pre-placed devices (reactors, generators), the player's +// starting weapons, and every prebuilt strut. Self-contained on purpose, so the +// dev UI can tear the world down and rebuild it from another map at runtime. +// +// NOTE: terrain is still a flat half-plane, so `ground_level` moves the ground +// up and down but cannot change its SHAPE. Real terrain is M6. +struct MapDef { + std::string name; + std::string description; + float ground_level = 1.0f; std::vector> deposits; - std::vector devices; + std::vector devices; + std::vector weapons; + std::vector beams; }; std::vector load_devices(ScriptEngine& script, const std::string& data_dir); -MapData load_map(ScriptEngine& script, const std::string& data_dir); + +// Loads every map from /maps.lua (+ mods), same layering as the rest. +std::vector load_maps(ScriptEngine& script, const std::string& data_dir); + +// Clears `graph` and rebuilds it from `m`: sets ground_level, then welds every +// prebuilt beam (creating/merging endpoint nodes, ground-level ones becoming +// foundations). Requires graph.materials to be loaded. +void build_map(const MapDef& m, BuildGraph& graph); -- cgit v1.3