1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
#pragma once
#include <vector>
#include <cstdint>
#include <string>
#include <unordered_set>
// 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<MaterialDef> 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<BuildNode> nodes;
std::vector<BuildEdge> edges;
std::vector<BuildFace> faces;
struct Destroyed { float x, y; };
std::vector<Destroyed> destroyed_events; // drained by the app for FX
private:
std::vector<std::vector<std::pair<int,int>>> adj_; // node -> (neighbour, edge)
std::unordered_set<uint64_t> edge_set_;
std::vector<float> fx_, fy_; // per-node force scratch
std::vector<char> held_; // per-node build-grace freeze
void rebuild_topology(); // adj_, edge_set_, faces, triangulated, node mass
bool edge_exists(int a, int b) const;
};
|