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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#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 cost_metal; // metal spent placing one strut of this material
float cost_energy; // energy spent placing one strut of this material
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; };
// A device mounted on the strut graph via two mount nodes. It is destroyed when
// (a) both mount nodes are removed (fall below ground, snap, break), or
// (b) its HP reaches zero from splash/beam/fire damage.
struct MountedDevice {
int node_a, node_b; // mount-point node indices (-1 if destroyed)
int type; // index into DeviceDefs (from data.hpp)
int team = 0; // team index (0 = player, 1 = enemy)
float hp, max_hp;
bool alive = true;
};
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);
// Directional velocity kick to free nodes within `radius` of (x,y), along the
// unit vector (dx,dy), with linear falloff. Weapon recoil uses this to shove
// the FIRING structure back (Forts' Recoil), unlike apply_splash's radial
// knockback on the target. No damage: recoil only moves the fort.
void apply_recoil(float x, float y, float radius,
float dx, float dy, float strength);
// 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(); }
// --- devices mounted on the strut graph ------------------------------
// Attach a device to two mount nodes (any type from DeviceDefs).
// Returns the device index in the graph's devices vector.
int mount_device(int na, int nb, int device_type, int team, float hp);
// Remove a device (e.g. when a weapon shot destroys it outright).
// Does NOT shift indices — marks it dead and skips it in queries.
void unmount_device(int device_id);
bool is_device_alive(int device_id) const;
float get_device_hp(int device_id) const;
const std::vector<MountedDevice>& get_devices() const { return devices; }
int nearest_device(float x, float y, float radius) const;
// Deal damage to a device (splash/beam/fire). Returns true if the device
// was alive before and is dead after (for downstream win-condition checks).
bool damage_device(int device_id, float amount);
// --- 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;
std::vector<MountedDevice> devices;
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;
};
|