aboutsummaryrefslogtreecommitdiffstats
path: root/src/game/data.cpp
diff options
context:
space:
mode:
authorVaino Kauppila <vaino@vke.fi>2026-07-02 22:58:25 +0300
committerVaino Kauppila <vaino@vke.fi>2026-07-02 23:47:07 +0300
commitcd0c08dc7ce754473178ad3f32f7740a1dddc1eb (patch)
tree589c676b41d6f3d322ea5ce339ddb0811f29ec31 /src/game/data.cpp
downloadforts_clone-cd0c08dc7ce754473178ad3f32f7740a1dddc1eb.tar.gz
forts_clone-cd0c08dc7ce754473178ad3f32f7740a1dddc1eb.zip
Initial import: LibreForts (M0-M5 in progress)
Open-source Forts clone: custom C++20 engine + data-driven (Lua) 2D physics artillery RTS. - Building: node/strut graph on a stiff mass-spring solver (canon Forts model), triangulation rigidity, axial + 30-degree angle-stress breaking, cascading collapse, fire (DoT + spread), ground destroys debris. - Weapons (M4): cannon (ballistic) + laser (beam/ignite) with select-and-aim-in- arc UX; splash / beam damage; bg-brace passthrough. - Devices + economy (M5): reactors + win/loss + restart, mines/turbines/battery, metal deposits, storage caps, per-shot energy cost. - Data-driven via Lua/sol2 with a layered mod loader; enemy-fort scenario mod. - Renderer: SDL3 + textures + ImGui dev UI. stb_image + ImGui vendored. - Assets: CC0 placeholders only; real game art loaded at runtime from the user's own install via forts: paths (bring-your-own; nothing copyrighted committed). See README.md / CLAUDE.md and research/ + requirements/ for detail.
Diffstat (limited to 'src/game/data.cpp')
-rw-r--r--src/game/data.cpp261
1 files changed, 261 insertions, 0 deletions
diff --git a/src/game/data.cpp b/src/game/data.cpp
new file mode 100644
index 0000000..54f7fdb
--- /dev/null
+++ b/src/game/data.cpp
@@ -0,0 +1,261 @@
+#include "game/data.hpp"
+#include "engine/script.hpp"
+
+#include <filesystem>
+#include <algorithm>
+#include <cstdio>
+
+// sol2's string-literal table keys trip a GCC -Warray-bounds false positive
+// under inlining/optimisation. The access is correct; silence the noise here.
+#if defined(__GNUC__) && !defined(__clang__)
+#pragma GCC diagnostic ignored "-Warray-bounds"
+#endif
+
+namespace fs = std::filesystem;
+
+namespace {
+constexpr float DEG2RAD = 3.14159265358979323846f / 180.0f;
+
+MaterialDef read_material(const sol::table& m) {
+ MaterialDef d{};
+ d.name = m.get_or("name", std::string("unnamed"));
+ d.stiffness = m.get_or("stiffness", 600.0f);
+ d.damping = m.get_or("damping", 12.0f);
+ d.mass = m.get_or("mass", 0.25f);
+ d.max_compression = m.get_or("max_compression", 0.90f);
+ d.max_expansion = m.get_or("max_expansion", 1.10f);
+ d.angle_threshold = m.get_or("angle_threshold", 30.0f) * DEG2RAD; // data is degrees
+ d.min_length = m.get_or("min_length", 0.4f);
+ d.max_length = m.get_or("max_length", 3.0f);
+ d.max_link_length = m.get_or("max_link_length", 6.0f);
+ d.hit_points = m.get_or("hit_points", 150.0f);
+ d.flammable = m.get_or("flammable", true);
+ d.burn_rate = m.get_or("burn_rate", 15.0f);
+ d.spread_time = m.get_or("spread_time", 2.0f);
+ d.blocks_projectiles = m.get_or("blocks_projectiles", true);
+ d.blocks_beam = m.get_or("blocks_beam", true);
+ d.tension_only = m.get_or("tension_only", false);
+ d.half_width = m.get_or("half_width", 0.15f);
+ d.texture = m.get_or("texture", std::string(""));
+
+ sol::optional<sol::table> col = m["color"];
+ d.r = col ? col->get_or(1, 0.8f) : 0.8f;
+ d.g = col ? col->get_or(2, 0.5f) : 0.5f;
+ d.b = col ? col->get_or(3, 0.2f) : 0.2f;
+ d.a = col ? col->get_or(4, 1.0f) : 1.0f;
+ return d;
+}
+} // namespace
+
+std::vector<MaterialDef> load_materials(ScriptEngine& script,
+ const std::string& data_dir) {
+ std::vector<MaterialDef> out;
+
+ // 1. Base data.
+ if (!script.run_file(data_dir + "/materials.lua")) return out;
+
+ // 2. Mods, ordered by (priority asc, path). Each mod.lua sets Priority;
+ // each materials.lua mutates the shared global Materials table.
+ std::error_code ec;
+ std::string mods_dir = data_dir + "/mods";
+ std::vector<std::pair<int, std::string>> mods;
+ if (fs::is_directory(mods_dir, ec)) {
+ for (const auto& e : fs::directory_iterator(mods_dir, ec)) {
+ if (!e.is_directory()) continue;
+ std::string mp = e.path().string();
+ if (!fs::exists(mp + "/materials.lua")) continue;
+ int priority = 5;
+ if (fs::exists(mp + "/mod.lua") && script.run_file(mp + "/mod.lua")) {
+ sol::optional<int> p = script.lua()["Priority"];
+ if (p) priority = *p;
+ }
+ mods.emplace_back(priority, mp);
+ }
+ std::sort(mods.begin(), mods.end());
+ for (const auto& [prio, mp] : mods) {
+ printf("Mod: applying %s (priority %d)\n", mp.c_str(), prio);
+ script.run_file(mp + "/materials.lua");
+ }
+ }
+
+ // 3. Read the final Materials array (1-based Lua array part, in order).
+ sol::optional<sol::table> mats = script.lua()["Materials"];
+ if (!mats) {
+ fprintf(stderr, "load_materials: no global 'Materials' table\n");
+ return out;
+ }
+ for (std::size_t i = 1; ; i++) {
+ sol::object o = (*mats)[i];
+ if (o.get_type() != sol::type::table) break;
+ out.push_back(read_material(o.as<sol::table>()));
+ }
+ printf("Loaded %zu materials from Lua\n", out.size());
+ return out;
+}
+
+namespace {
+// Run <data_dir>/<file> then every mod's <file> (priority, name order).
+void run_layered(ScriptEngine& script, const std::string& data_dir,
+ const std::string& file) {
+ if (!script.run_file(data_dir + "/" + file)) return;
+ std::error_code ec;
+ std::string mods_dir = data_dir + "/mods";
+ std::vector<std::pair<int, std::string>> mods;
+ if (fs::is_directory(mods_dir, ec)) {
+ for (const auto& e : fs::directory_iterator(mods_dir, ec)) {
+ if (!e.is_directory()) continue;
+ std::string mp = e.path().string();
+ if (!fs::exists(mp + "/" + file)) continue;
+ int priority = 5;
+ if (fs::exists(mp + "/mod.lua") && script.run_file(mp + "/mod.lua")) {
+ sol::optional<int> p = script.lua()["Priority"];
+ if (p) priority = *p;
+ }
+ mods.emplace_back(priority, mp);
+ }
+ std::sort(mods.begin(), mods.end());
+ for (const auto& [prio, mp] : mods)
+ script.run_file(mp + "/" + file);
+ }
+}
+
+WeaponDef read_weapon(const sol::table& w) {
+ WeaponDef d{};
+ d.name = w.get_or("name", std::string("unnamed"));
+ d.muzzle_speed = w.get_or("muzzle_speed", 35.0f);
+ d.reload = w.get_or("reload", 2.0f);
+ d.splash_damage = w.get_or("splash_damage", 200.0f);
+ d.splash_radius = w.get_or("splash_radius", 3.0f);
+ d.knockback = w.get_or("knockback", 60.0f);
+ d.projectile_radius = w.get_or("projectile_radius", 0.15f);
+ d.fire_chance = w.get_or("fire_chance", 0.0f);
+ d.min_fire_angle = w.get_or("min_fire_angle", -20.0f) * DEG2RAD;
+ d.max_fire_angle = w.get_or("max_fire_angle", 30.0f) * DEG2RAD;
+ d.is_beam = w.get_or("is_beam", false);
+ d.beam_range = w.get_or("beam_range", 60.0f);
+ d.beam_dps = w.get_or("beam_dps", 120.0f);
+ d.beam_duration = w.get_or("beam_duration", 1.5f);
+ d.energy_cost = w.get_or("energy_cost", 0.0f);
+ d.texture = w.get_or("texture", std::string(""));
+ d.projectile_texture = w.get_or("projectile_texture", std::string(""));
+ return d;
+}
+} // namespace
+
+std::vector<WeaponDef> load_weapons(ScriptEngine& script, const std::string& data_dir) {
+ std::vector<WeaponDef> out;
+ run_layered(script, data_dir, "weapons.lua");
+ sol::optional<sol::table> ws = script.lua()["Weapons"];
+ if (!ws) { fprintf(stderr, "load_weapons: no global 'Weapons' table\n"); return out; }
+ for (std::size_t i = 1; ; i++) {
+ sol::object o = (*ws)[i];
+ if (o.get_type() != sol::type::table) break;
+ out.push_back(read_weapon(o.as<sol::table>()));
+ }
+ printf("Loaded %zu weapons from Lua\n", out.size());
+ return out;
+}
+
+void load_structures(ScriptEngine& script, const std::string& data_dir,
+ BuildGraph& graph) {
+ run_layered(script, data_dir, "structures.lua");
+ sol::optional<sol::table> structs = script.lua()["Structures"];
+ if (!structs) return;
+
+ auto mat_index = [&](const std::string& nm) -> int {
+ for (int i = 0; i < (int)graph.materials.size(); i++)
+ if (graph.materials[i].name == nm) return i;
+ return 0;
+ };
+ auto node_at = [&](float x, float y) -> int {
+ int n = graph.find_nearest_node(x, y, BuildGraph::SNAP_RADIUS);
+ return (n >= 0) ? n : graph.add_node(x, y, graph.is_on_ground(y));
+ };
+
+ 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::table>();
+ sol::optional<sol::table> 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<sol::table>();
+ 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 (auto& e : graph.edges) e.settle = 0.0f; // prebuilt = already cured
+ printf("Loaded %d prebuilt struts from Structures\n", built);
+}
+
+namespace {
+DeviceDef read_device(const sol::table& d) {
+ DeviceDef v{};
+ v.name = d.get_or("name", std::string("device"));
+ v.hp = d.get_or("hp", 100.0f);
+ v.energy_rate = d.get_or("energy_rate", 0.0f);
+ v.metal_rate = d.get_or("metal_rate", 0.0f);
+ v.energy_storage = d.get_or("energy_storage", 0.0f);
+ v.metal_storage = d.get_or("metal_storage", 0.0f);
+ v.needs_deposit = d.get_or("needs_deposit", false);
+ v.wind = d.get_or("wind", false);
+ v.wind_max_rate = d.get_or("wind_max_rate", 15.0f);
+ v.wind_max_height = d.get_or("wind_max_height", 20.0f);
+ v.is_core = d.get_or("is_core", false);
+ v.half_w = d.get_or("half_w", 0.6f);
+ v.half_h = d.get_or("half_h", 0.6f);
+ v.cost_metal = d.get_or("cost_metal", 0.0f);
+ v.cost_energy = d.get_or("cost_energy", 0.0f);
+ v.texture = d.get_or("texture", std::string(""));
+ sol::optional<sol::table> col = d["color"];
+ v.r = col ? col->get_or(1, 0.8f) : 0.8f;
+ v.g = col ? col->get_or(2, 0.8f) : 0.8f;
+ v.b = col ? col->get_or(3, 0.8f) : 0.8f;
+ return v;
+}
+} // namespace
+
+std::vector<DeviceDef> load_devices(ScriptEngine& script, const std::string& data_dir) {
+ std::vector<DeviceDef> out;
+ run_layered(script, data_dir, "devices.lua");
+ sol::optional<sol::table> ds = script.lua()["DeviceDefs"];
+ if (!ds) { fprintf(stderr, "load_devices: no 'DeviceDefs' table\n"); return out; }
+ for (std::size_t i = 1; ; i++) {
+ sol::object o = (*ds)[i];
+ if (o.get_type() != sol::type::table) break;
+ out.push_back(read_device(o.as<sol::table>()));
+ }
+ printf("Loaded %zu devices from Lua\n", out.size());
+ return out;
+}
+
+MapData load_map(ScriptEngine& script, const std::string& data_dir) {
+ MapData m;
+ run_layered(script, data_dir, "map.lua");
+ if (sol::optional<sol::table> dep = script.lua()["Deposits"]) {
+ for (std::size_t i = 1; ; i++) {
+ sol::object o = (*dep)[i];
+ if (o.get_type() != sol::type::table) break;
+ sol::table p = o.as<sol::table>();
+ m.deposits.emplace_back(p.get_or(1, 0.0f), p.get_or(2, 0.0f));
+ }
+ }
+ if (sol::optional<sol::table> md = script.lua()["MapDevices"]) {
+ for (std::size_t i = 1; ; i++) {
+ sol::object o = (*md)[i];
+ if (o.get_type() != sol::type::table) break;
+ sol::table d = o.as<sol::table>();
+ m.devices.push_back({ d.get_or("type", std::string("reactor")),
+ d.get_or("x", 0.0f), d.get_or("y", 0.0f),
+ d.get_or("team", 0) });
+ }
+ }
+ printf("Loaded map: %zu deposits, %zu devices\n", m.deposits.size(), m.devices.size());
+ return m;
+}