From cd0c08dc7ce754473178ad3f32f7740a1dddc1eb Mon Sep 17 00:00:00 2001 From: Vaino Kauppila Date: Thu, 2 Jul 2026 22:58:25 +0300 Subject: 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. --- src/game/build_graph.hpp | 151 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/game/build_graph.hpp (limited to 'src/game/build_graph.hpp') diff --git a/src/game/build_graph.hpp b/src/game/build_graph.hpp new file mode 100644 index 0000000..f92dbd3 --- /dev/null +++ b/src/game/build_graph.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include + +// The building structure is a single graph of nodes (point masses) joined by +// edges (struts). The solver is a stiff damped mass-spring system — the canon +// Forts model: each strut is a Hookean spring (F = k.dx) with damping, and the +// stiff springs are integrated with oversampling for stability. +// +// There is ONE index space (node and edge indices). Rigidity is geometric +// (triangulation): a lone strut is a free-hinging pin joint that SNAPS once it +// rotates past its material's angle threshold (Forts' 30-degree rule), or once +// it deforms axially past MaxCompression/MaxExpansion. + +// Material definition — loaded from Lua data (see game/data.cpp). +struct MaterialDef { + float stiffness; // spring constant k (force per unit stretch) + float damping; // axial spring damping + float mass; // contributed to each endpoint node + float max_compression; // snap if length/rest < this (e.g. 0.90 = -10%) + float max_expansion; // snap if length/rest > this (e.g. 1.10 = +10%) + float angle_threshold; // snap a loose strut rotated this far (radians) + float min_length; // shortest a single strut may be (placement rejected below) + float max_length; // longest a single segment (a longer drag subdivides) + float max_link_length; // longest a whole drag/link (rejected beyond) + float hit_points; // HP pool depleted by weapon damage (separate from stress) + bool tension_only; // ropes: slack (no force, no snap) in compression + bool flammable; // can catch fire + float burn_rate; // HP lost per second while burning + float spread_time; // seconds of burning before it ignites a neighbour + bool blocks_projectiles; // stops cannon shells (wood yes; bg-brace/rope no) + bool blocks_beam; // stops laser beams (all yes in Forts) + float half_width; // visual thickness + float r, g, b, a; // visual colour (also tints the texture) + std::string name; + std::string texture; // optional texture path ("" = flat colour) +}; + +struct BuildNode { + float x, y; // position + float vx, vy; // velocity + float mass; // aggregate of incident struts (>= MIN_NODE_MASS) + bool is_foundation; // pinned to the ground +}; + +struct BuildEdge { + int node_a, node_b; // node indices + int mat; // index into MATERIALS + float rest_length; // captured at build time + float rest_angle; // world orientation at build time + float stress; // signed deformation (length-rest)/rest, for colour + float hp, max_hp; // weapon-damage health pool + bool burning; // on fire (takes damage-over-time, spreads) + float burn; // seconds spent burning (drives spread timing) + float settle; // build-grace timer (s); while >0 the strut is held rigid + bool triangulated; // cached: part of a triangle (rigid) — set by topology + float half_width; // cached from material (render) + float r, g, b, a; // cached from material (render) +}; + +struct BuildFace { int node_a, node_b, node_c; }; + +class BuildGraph { +public: + // Material table, loaded from Lua at startup (build modes index into it). + std::vector materials; + + // --- tunables --------------------------------------------------------- + static constexpr float SETTLE_TIME = 4.0f; // rigid build grace (Forts TempBracing) + static constexpr float SNAP_RADIUS = 0.4f; // node merge distance + static constexpr float MIN_BRACE_LENGTH = 0.6f; // smallest auto brace + static constexpr int OVERSAMPLES = 14; // spring substeps per tick + static constexpr float MIN_NODE_MASS = 0.05f; + + float ground_level = 1.0f; // below = solid ground, above = sky + float gravity = 10.0f; // world units / s^2 + float air_drag = 0.8f; // linear velocity drag coefficient + + // --- construction ----------------------------------------------------- + int add_node(float x, float y, bool foundation = false); + int add_edge(int na, int nb, int mat); + + // Place a strut between two nodes, subdividing into segments no longer than + // the material's max_length (intermediate nodes auto-created). Rejects links + // outside [min_length, max_link_length]. Returns segments created (0 = none). + int add_link(int na, int nb, int mat); + + void extrude_edge(int edge_id, float off_x, float off_y); + + // --- simulation (call inside the fixed timestep) ---------------------- + void step(float dt); // integrate the mass-spring system (oversampled) + int check_strain(); // snap struts past axial / angle limits + void update_timers(float dt); // build-grace countdown + int kill_grounded(); // destroy debris below the ground + + // --- weapon damage ---------------------------------------------------- + // Radial splash at (x,y): damages struts (HP, linear falloff), applies + // knockback to nodes, and breaks struts whose HP hits zero. Structure does + // not block splash (pure radius falloff, like Forts AoE). + void apply_splash(float x, float y, float radius, float damage, float knockback); + + // Trace a laser beam from (ox,oy) along (dx,dy) up to `range`: damage + + // optionally ignite EVERY strut it crosses, passing through beam-transparent + // materials (bg-brace, ropes) and STOPPING at the first blocking one (wood). + // Fills the stop point and returns the beam length. + float beam_fire(float ox, float oy, float dx, float dy, float range, + float damage, bool ignite, float& hit_x, float& hit_y); + + // Nearest edge to (x,y) within radius that blocks projectiles (what a + // cannon shell detonates on). Returns edge index or -1. + int find_blocking_edge(float x, float y, float radius) const; + + // --- fire ------------------------------------------------------------- + void ignite_edge(int edge_id); // set a strut on fire (if flammable) + void ignite_area(float x, float y, float radius); // ignite flammable struts in radius + void update_fire(float dt); // burn DoT + spread + destroy at 0 HP + + // Recompute adjacency/faces/masses (call after directly assigning nodes/edges, + // e.g. restoring a scenario snapshot). + void rebuild() { rebuild_topology(); } + + // --- destruction primitives ------------------------------------------- + void break_edge(int edge_id); + void break_node(int node_id); + + // --- queries ---------------------------------------------------------- + int find_nearest_node(float x, float y, float radius = 0.5f) const; + int find_nearest_edge(float x, float y, float radius = 0.5f) const; + bool is_on_ground(float y) const { return y <= ground_level + 0.3f; } + BuildEdge* mutable_edge(int id); + + // --- data ------------------------------------------------------------- + std::vector nodes; + std::vector edges; + std::vector faces; + + struct Destroyed { float x, y; }; + std::vector destroyed_events; // drained by the app for FX + +private: + std::vector>> adj_; // node -> (neighbour, edge) + std::unordered_set edge_set_; + std::vector fx_, fy_; // per-node force scratch + std::vector held_; // per-node build-grace freeze + + void rebuild_topology(); // adj_, edge_set_, faces, triangulated, node mass + bool edge_exists(int a, int b) const; +}; -- cgit v1.3