diff options
Diffstat (limited to 'src/game/build_graph.cpp')
| -rw-r--r-- | src/game/build_graph.cpp | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/src/game/build_graph.cpp b/src/game/build_graph.cpp new file mode 100644 index 0000000..032bbf5 --- /dev/null +++ b/src/game/build_graph.cpp @@ -0,0 +1,487 @@ +#include "game/build_graph.hpp" +#include <cmath> +#include <cstdio> +#include <algorithm> + +namespace { +constexpr float PI = 3.14159265358979323846f; +uint64_t edge_key(int a, int b) { + if (a > b) std::swap(a, b); + return (uint64_t)(uint32_t)a << 32 | (uint32_t)b; +} +float wrap_angle(float d) { + while (d > PI) d -= 2.0f * PI; + while (d < -PI) d += 2.0f * PI; + return d; +} +} + +// ============================================================================= +// Construction +// ============================================================================= + +int BuildGraph::add_node(float x, float y, bool foundation) { + nodes.push_back({x, y, 0.0f, 0.0f, MIN_NODE_MASS, foundation}); + return (int)nodes.size() - 1; +} + +int BuildGraph::add_edge(int na, int nb, int mat) { + if (na < 0 || na >= (int)nodes.size()) return -1; + if (nb < 0 || nb >= (int)nodes.size()) return -1; + if (na == nb || edge_exists(na, nb)) return -1; + if (materials.empty()) return -1; + if (mat < 0 || mat >= (int)materials.size()) mat = 0; + + const BuildNode& A = nodes[na]; + const BuildNode& B = nodes[nb]; + float dx = B.x - A.x, dy = B.y - A.y; + const MaterialDef& m = materials[mat]; + + BuildEdge e{}; + e.node_a = na; + e.node_b = nb; + e.mat = mat; + e.rest_length = std::sqrt(dx*dx + dy*dy); + e.rest_angle = std::atan2(dy, dx); + e.stress = 0.0f; + e.hp = e.max_hp = m.hit_points; + e.burning = false; + e.burn = 0.0f; + e.settle = m.tension_only ? 0.0f : SETTLE_TIME; // ropes are floppy by nature + e.half_width = m.half_width; + e.r = m.r; e.g = m.g; e.b = m.b; e.a = m.a; + edges.push_back(e); + + rebuild_topology(); + return (int)edges.size() - 1; +} + +int BuildGraph::add_link(int na, int nb, int mat) { + if (na < 0 || na >= (int)nodes.size()) return 0; + if (nb < 0 || nb >= (int)nodes.size()) return 0; + if (na == nb || materials.empty()) return 0; + if (mat < 0 || mat >= (int)materials.size()) mat = 0; + const MaterialDef& m = materials[mat]; + + float ax = nodes[na].x, ay = nodes[na].y; + float bx = nodes[nb].x, by = nodes[nb].y; + float dist = std::hypot(bx - ax, by - ay); + if (dist < m.min_length || dist > m.max_link_length) return 0; + + int segs = (m.max_length > 0.0f) ? (int)std::ceil(dist / m.max_length) : 1; + if (segs < 1) segs = 1; + if (segs == 1) return add_edge(na, nb, mat) >= 0 ? 1 : 0; + + // Subdivide into a chain of intermediate nodes so no segment exceeds max_length. + int created = 0, prev = na; + for (int i = 1; i < segs; i++) { + float t = (float)i / segs; + float px = ax + (bx - ax) * t, py = ay + (by - ay) * t; + int mid = add_node(px, py, is_on_ground(py)); + if (add_edge(prev, mid, mat) >= 0) created++; + prev = mid; + } + if (add_edge(prev, nb, mat) >= 0) created++; + return created; +} + +void BuildGraph::extrude_edge(int edge_id, float off_x, float off_y) { + if (edge_id < 0 || edge_id >= (int)edges.size()) return; + + int mat0 = edges[edge_id].mat; + if (mat0 >= 0 && mat0 < (int)materials.size()) { + // Keep box sides single (braceable) edges: clamp the drag to max_length. + float ol = std::sqrt(off_x*off_x + off_y*off_y); + float maxlen = materials[mat0].max_length; + if (ol > maxlen && ol > 1e-6f) { off_x *= maxlen / ol; off_y *= maxlen / ol; } + } + if (std::sqrt(off_x*off_x + off_y*off_y) < 0.1f) return; + + const BuildEdge e = edges[edge_id]; // snapshot before vectors move + const int ea = e.node_a, eb = e.node_b, mat = e.mat; + const float ax = nodes[ea].x, ay = nodes[ea].y; + const float bx = nodes[eb].x, by = nodes[eb].y; + + float cx = ax + off_x, cy = ay + off_y; // slanted parallelogram is fine + float dx = bx + off_x, dy = by + off_y; + + // Node merging: reuse a nearby existing node so the box grafts into the graph. + int nc = find_nearest_node(cx, cy, SNAP_RADIUS); + if (nc < 0 || nc == ea || nc == eb) nc = add_node(cx, cy, is_on_ground(cy)); + int nd = find_nearest_node(dx, dy, SNAP_RADIUS); + if (nd < 0 || nd == ea || nd == eb || nd == nc) nd = add_node(dx, dy, is_on_ground(dy)); + + add_edge(nc, nd, mat); // three new sides (original edge is the fourth) + add_edge(ea, nc, mat); + add_edge(eb, nd, mat); + + // Auto diagonal brace (longer diagonal) so the box is rigid on creation. + if (!materials[mat].tension_only) { + float l1 = std::hypot(nodes[nd].x - nodes[ea].x, nodes[nd].y - nodes[ea].y); + float l2 = std::hypot(nodes[nc].x - nodes[eb].x, nodes[nc].y - nodes[eb].y); + if (l1 >= l2) { if (l1 >= MIN_BRACE_LENGTH) add_edge(ea, nd, mat); } + else { if (l2 >= MIN_BRACE_LENGTH) add_edge(eb, nc, mat); } + } +} + +// ============================================================================= +// Topology — adjacency, edge lookup, faces, triangulation, node mass O(E.deg) +// ============================================================================= + +void BuildGraph::rebuild_topology() { + adj_.assign(nodes.size(), {}); + edge_set_.clear(); + for (int ei = 0; ei < (int)edges.size(); ei++) { + const auto& e = edges[ei]; + adj_[e.node_a].push_back({e.node_b, ei}); + adj_[e.node_b].push_back({e.node_a, ei}); + edge_set_.insert(edge_key(e.node_a, e.node_b)); + } + + // Faces + per-edge triangulated flag (common neighbour of both endpoints). + faces.clear(); + for (auto& e : edges) { + int a = std::min(e.node_a, e.node_b); + int b = std::max(e.node_a, e.node_b); + bool tri = false; + for (auto [c, _] : adj_[a]) { + if (c == b) continue; + if (edge_set_.count(edge_key(b, c))) { + tri = true; + if (c > b) faces.push_back({a, b, c}); // emit each triangle once + } + } + e.triangulated = tri; + } + + // Node mass = half of each incident strut's mass, floored. + for (auto& n : nodes) n.mass = 0.0f; + for (const auto& e : edges) { + float half = materials[e.mat].mass * 0.5f; + nodes[e.node_a].mass += half; + nodes[e.node_b].mass += half; + } + for (auto& n : nodes) if (n.mass < MIN_NODE_MASS) n.mass = MIN_NODE_MASS; +} + +bool BuildGraph::edge_exists(int a, int b) const { + return edge_set_.count(edge_key(a, b)) != 0; +} + +// ============================================================================= +// Simulation — stiff damped mass-spring, integrated with oversampling +// ============================================================================= + +void BuildGraph::step(float dt) { + const int n = (int)nodes.size(); + const float h = dt / OVERSAMPLES; + float dampf = 1.0f - air_drag * h; + if (dampf < 0.0f) dampf = 0.0f; + + // Fresh struts are held rigid during their build grace: freeze their nodes. + held_.assign(n, 0); + for (const auto& e : edges) { + if (e.settle <= 0.0f) continue; + if (!nodes[e.node_a].is_foundation) held_[e.node_a] = 1; + if (!nodes[e.node_b].is_foundation) held_[e.node_b] = 1; + } + + for (int s = 0; s < OVERSAMPLES; s++) { + fx_.assign(n, 0.0f); + fy_.assign(n, 0.0f); + + // gravity (held/pinned nodes don't move, so skip) + for (int i = 0; i < n; i++) + if (!nodes[i].is_foundation && !held_[i]) + fy_[i] = -gravity * nodes[i].mass; + + // spring forces: F = k*stretch + c*(relative velocity along axis) + for (const auto& e : edges) { + BuildNode& A = nodes[e.node_a]; + BuildNode& B = nodes[e.node_b]; + float dx = B.x - A.x, dy = B.y - A.y; + float dist = std::sqrt(dx*dx + dy*dy); + if (dist < 1e-6f) continue; + float nx = dx / dist, ny = dy / dist; + float stretch = dist - e.rest_length; + const MaterialDef& m = materials[e.mat]; + if (m.tension_only && stretch < 0.0f) continue; // rope slack + + float relv = (B.vx - A.vx) * nx + (B.vy - A.vy) * ny; + float f = m.stiffness * stretch + m.damping * relv; + fx_[e.node_a] += f * nx; fy_[e.node_a] += f * ny; + fx_[e.node_b] -= f * nx; fy_[e.node_b] -= f * ny; + } + + // semi-implicit Euler integration + for (int i = 0; i < n; i++) { + BuildNode& p = nodes[i]; + if (p.is_foundation || held_[i]) { p.vx = p.vy = 0.0f; continue; } + p.vx = (p.vx + fx_[i] / p.mass * h) * dampf; + p.vy = (p.vy + fy_[i] / p.mass * h) * dampf; + p.x += p.vx * h; + p.y += p.vy * h; + } + } + + // Record signed axial deformation for stress colouring. + for (auto& e : edges) { + float dx = nodes[e.node_b].x - nodes[e.node_a].x; + float dy = nodes[e.node_b].y - nodes[e.node_a].y; + float dist = std::sqrt(dx*dx + dy*dy); + e.stress = (e.rest_length > 1e-6f) ? (dist - e.rest_length) / e.rest_length : 0.0f; + } +} + +int BuildGraph::check_strain() { + std::vector<int> brk; + for (int ei = 0; ei < (int)edges.size(); ei++) { + const BuildEdge& e = edges[ei]; + const MaterialDef& m = materials[e.mat]; + const BuildNode& A = nodes[e.node_a]; + const BuildNode& B = nodes[e.node_b]; + float dx = B.x - A.x, dy = B.y - A.y; + float dist = std::sqrt(dx*dx + dy*dy); + float ratio = (e.rest_length > 1e-6f) ? dist / e.rest_length : 1.0f; + + bool fail = false; + if (m.tension_only) { + fail = ratio > m.max_expansion; // rope: over-stretch only + } else { + fail = ratio < m.max_compression || ratio > m.max_expansion; + // Angle stress: a loose (un-triangulated) strut past its grace snaps + // once it rotates too far from the angle it was built at. + if (!fail && !e.triangulated && e.settle <= 0.0f) { + float dev = std::fabs(wrap_angle(std::atan2(dy, dx) - e.rest_angle)); + if (dev > m.angle_threshold) fail = true; + } + } + if (fail) brk.push_back(ei); + } + if (brk.empty()) return 0; + + for (auto it = brk.rbegin(); it != brk.rend(); ++it) // descending: indices stay valid + edges.erase(edges.begin() + *it); + rebuild_topology(); + printf("Build: %zu strut(s) snapped\n", brk.size()); + return (int)brk.size(); +} + +void BuildGraph::update_timers(float dt) { + for (auto& e : edges) + if (e.settle > 0.0f) + e.settle = e.triangulated ? 0.0f : std::max(0.0f, e.settle - dt); +} + +int BuildGraph::kill_grounded() { + int killed = 0; + for (;;) { + int hit = -1; + for (int i = 0; i < (int)nodes.size(); i++) + if (!nodes[i].is_foundation && nodes[i].y < ground_level) { hit = i; break; } + if (hit < 0) break; + destroyed_events.push_back({nodes[hit].x, nodes[hit].y}); + break_node(hit); + killed++; + } + return killed; +} + +// ============================================================================= +// Destruction +// ============================================================================= + +void BuildGraph::apply_splash(float x, float y, float radius, float damage, + float knockback) { + if (radius <= 0.0f) return; + + // Knockback: shove nearby free nodes away from the blast (linear falloff). + for (auto& n : nodes) { + if (n.is_foundation) continue; + float dx = n.x - x, dy = n.y - y; + float d = std::sqrt(dx*dx + dy*dy); + if (d >= radius) continue; + float f = 1.0f - d / radius; + if (d > 1e-4f) { n.vx += (dx/d) * knockback * f; n.vy += (dy/d) * knockback * f; } + } + + // Damage struts by their midpoint distance; collect those that hit 0 HP. + std::vector<int> brk; + for (int ei = 0; ei < (int)edges.size(); ei++) { + auto& e = edges[ei]; + float mx = (nodes[e.node_a].x + nodes[e.node_b].x) * 0.5f; + float my = (nodes[e.node_a].y + nodes[e.node_b].y) * 0.5f; + float d = std::sqrt((mx-x)*(mx-x) + (my-y)*(my-y)); + if (d >= radius) continue; + e.hp -= damage * (1.0f - d / radius); + if (e.hp <= 0.0f) brk.push_back(ei); + } + if (brk.empty()) return; + for (auto it = brk.rbegin(); it != brk.rend(); ++it) + edges.erase(edges.begin() + *it); + rebuild_topology(); +} + +int BuildGraph::find_blocking_edge(float x, float y, float radius) const { + int best = -1; float best_d = radius; + for (int i = 0; i < (int)edges.size(); i++) { + if (!materials[edges[i].mat].blocks_projectiles) continue; + const BuildNode& A = nodes[edges[i].node_a]; + const BuildNode& B = nodes[edges[i].node_b]; + float ex = B.x - A.x, ey = B.y - A.y; + float len2 = ex*ex + ey*ey; + if (len2 < 1e-6f) continue; + float t = std::clamp(((x-A.x)*ex + (y-A.y)*ey) / len2, 0.0f, 1.0f); + float d = std::hypot(x - (A.x + t*ex), y - (A.y + t*ey)); + if (d < best_d) { best_d = d; best = i; } + } + return best; +} + +float BuildGraph::beam_fire(float ox, float oy, float dx, float dy, float range, + float damage, bool ignite, float& hit_x, float& hit_y) { + float dl = std::hypot(dx, dy); + if (dl < 1e-6f) { hit_x = ox; hit_y = oy; return 0.0f; } + dx /= dl; dy /= dl; + + // Gather every strut the ray crosses (of any material), sorted by distance. + struct Cross { float t; int edge; }; + std::vector<Cross> crosses; + for (int i = 0; i < (int)edges.size(); i++) { + const BuildNode& A = nodes[edges[i].node_a]; + const BuildNode& B = nodes[edges[i].node_b]; + float ex = B.x - A.x, ey = B.y - A.y; + float denom = dx * ey - dy * ex; + if (std::fabs(denom) < 1e-6f) continue; // parallel + float t = ((A.x - ox) * ey - (A.y - oy) * ex) / denom; // dist along ray + float u = ((A.x - ox) * dy - (A.y - oy) * dx) / denom; // param along segment + if (t >= 0.0f && t <= range && u >= 0.0f && u <= 1.0f) crosses.push_back({t, i}); + } + std::sort(crosses.begin(), crosses.end(), + [](const Cross& a, const Cross& b){ return a.t < b.t; }); + + // Damage/ignite each crossed strut; pass through transparent ones; stop at wood. + float stop = range; + std::vector<int> brk; + for (const auto& c : crosses) { + BuildEdge& e = edges[c.edge]; + e.hp -= damage; + if (ignite && materials[e.mat].flammable) e.burning = true; + if (e.hp <= 0.0f) brk.push_back(c.edge); + if (materials[e.mat].blocks_beam) { stop = c.t; break; } // wood halts the beam + } + hit_x = ox + dx * stop; + hit_y = oy + dy * stop; + + if (!brk.empty()) { + std::sort(brk.begin(), brk.end()); + brk.erase(std::unique(brk.begin(), brk.end()), brk.end()); + for (auto it = brk.rbegin(); it != brk.rend(); ++it) + edges.erase(edges.begin() + *it); + rebuild_topology(); + } + return stop; +} + +void BuildGraph::ignite_edge(int edge_id) { + if (edge_id < 0 || edge_id >= (int)edges.size()) return; + if (materials[edges[edge_id].mat].flammable) edges[edge_id].burning = true; +} + +void BuildGraph::ignite_area(float x, float y, float radius) { + for (int i = 0; i < (int)edges.size(); i++) { + float mx = (nodes[edges[i].node_a].x + nodes[edges[i].node_b].x) * 0.5f; + float my = (nodes[edges[i].node_a].y + nodes[edges[i].node_b].y) * 0.5f; + if (std::hypot(mx - x, my - y) < radius) ignite_edge(i); + } +} + +void BuildGraph::update_fire(float dt) { + // Burn: DoT + advance spread timer; ignite flammable neighbours; destroy at 0. + std::vector<int> newly_lit; + std::vector<int> brk; + for (int i = 0; i < (int)edges.size(); i++) { + auto& e = edges[i]; + if (!e.burning) continue; + const MaterialDef& m = materials[e.mat]; + e.hp -= m.burn_rate * dt; + e.burn += dt; + if (e.burn >= m.spread_time) { + e.burn = 0.0f; // spread again after each interval + for (int end : { e.node_a, e.node_b }) + for (auto [nb, ei] : adj_[end]) + if (!edges[ei].burning && materials[edges[ei].mat].flammable) + newly_lit.push_back(ei); + } + if (e.hp <= 0.0f) brk.push_back(i); + } + for (int ei : newly_lit) + if (ei >= 0 && ei < (int)edges.size()) edges[ei].burning = true; + if (!brk.empty()) { + std::sort(brk.begin(), brk.end()); + brk.erase(std::unique(brk.begin(), brk.end()), brk.end()); + for (auto it = brk.rbegin(); it != brk.rend(); ++it) + edges.erase(edges.begin() + *it); + rebuild_topology(); + } +} + +void BuildGraph::break_edge(int edge_id) { + if (edge_id < 0 || edge_id >= (int)edges.size()) return; + edges.erase(edges.begin() + edge_id); + rebuild_topology(); +} + +void BuildGraph::break_node(int node_id) { + if (node_id < 0 || node_id >= (int)nodes.size()) return; + + edges.erase(std::remove_if(edges.begin(), edges.end(), + [node_id](const BuildEdge& e) { + return e.node_a == node_id || e.node_b == node_id; + }), edges.end()); + + nodes.erase(nodes.begin() + node_id); + for (auto& e : edges) { + if (e.node_a > node_id) e.node_a--; + if (e.node_b > node_id) e.node_b--; + } + rebuild_topology(); +} + +// ============================================================================= +// Queries +// ============================================================================= + +int BuildGraph::find_nearest_node(float x, float y, float radius) const { + int best = -1; + float best_d2 = radius * radius; + for (int i = 0; i < (int)nodes.size(); i++) { + float dx = nodes[i].x - x, dy = nodes[i].y - y; + float d2 = dx*dx + dy*dy; + if (d2 < best_d2) { best_d2 = d2; best = i; } + } + return best; +} + +int BuildGraph::find_nearest_edge(float x, float y, float radius) const { + int best = -1; + float best_d = radius; + for (int i = 0; i < (int)edges.size(); i++) { + const BuildNode& A = nodes[edges[i].node_a]; + const BuildNode& B = nodes[edges[i].node_b]; + float ex = B.x - A.x, ey = B.y - A.y; + float len2 = ex*ex + ey*ey; + if (len2 < 0.0001f) continue; + float t = ((x - A.x)*ex + (y - A.y)*ey) / len2; + t = std::clamp(t, 0.0f, 1.0f); + float px = A.x + t*ex, py = A.y + t*ey; + float d = std::hypot(x - px, y - py); + if (d < best_d) { best_d = d; best = i; } + } + return best; +} + +BuildEdge* BuildGraph::mutable_edge(int id) { + if (id < 0 || id >= (int)edges.size()) return nullptr; + return &edges[id]; +} |
