#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; // CC0 placeholder path ("" = flat colour) std::string texture_forts; // preferred BYO Forts art ("forts:..."), falls // back to `texture` then colour when absent }; 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; };