aboutsummaryrefslogtreecommitdiffstats
path: root/src/game/build_graph.hpp
blob: cd8dc3d56232bff522efdb0e2b16eea324a9f8d9 (plain) (blame)
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;
};