aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/engine/app.cpp32
-rw-r--r--src/game/build_graph.cpp158
-rw-r--r--src/game/build_graph.hpp30
3 files changed, 212 insertions, 8 deletions
diff --git a/src/engine/app.cpp b/src/engine/app.cpp
index 0de71c5..cfe924c 100644
--- a/src/engine/app.cpp
+++ b/src/engine/app.cpp
@@ -148,7 +148,7 @@ void App::run() {
device_defs[i].texture);
SDL_Texture* deposit_tex = renderer.load_texture("data/textures/deposit.png");
- struct Device { int def, team; float x, y, hp, max_hp; bool on_deposit; };
+ struct Device { int def, team; float x, y, hp, max_hp; bool on_deposit; int graph_id = -1; };
std::vector<Device> devices;
auto devidx = [&](const char* n) -> int {
@@ -177,11 +177,24 @@ void App::run() {
int cur_mat = 0;
int place_dev = -1; // >=0 = placing that device type; -1 = build materials
+ // Mount a device onto the nearest strut so cutting its supports destroys
+ // it (win-condition path (c): reactor falls when its supports are cut).
+ // Devices with no nearby strut (radius 2.0) stay unmounted (-1) and are
+ // only killed by weapon damage, same as before this feature existed.
+ const float MOUNT_RADIUS = 2.0f;
+ auto mount_at = [&](float x, float y, int def_idx, int team, float hp) -> int {
+ int eid = graph.find_nearest_edge(x, y, MOUNT_RADIUS);
+ if (eid < 0) return -1;
+ const BuildEdge& e = graph.edges[eid];
+ return graph.mount_device(e.node_a, e.node_b, def_idx, team, hp);
+ };
+
auto reset = [&]() {
const MapDef& m = maps[current_map];
graph.nodes = snap_nodes;
graph.edges = snap_edges;
graph.rebuild();
+ graph.devices.clear();
devices.clear();
for (auto& pd : m.devices) {
int di = -1;
@@ -190,6 +203,7 @@ void App::run() {
if (di < 0) continue;
Device dv{di, pd.team, pd.x, pd.y, device_defs[di].hp, device_defs[di].hp, true};
if (device_defs[di].needs_deposit) dv.on_deposit = on_deposit(pd.x, pd.y);
+ dv.graph_id = mount_at(pd.x, pd.y, di, pd.team, device_defs[di].hp);
devices.push_back(dv);
}
cannons.clear();
@@ -317,8 +331,10 @@ void App::run() {
const DeviceDef& def = device_defs[place_dev];
bool dep_ok = !def.needs_deposit || on_deposit(wx, wy);
if (dep_ok && metal >= def.cost_metal && energy >= def.cost_energy) {
- devices.push_back({ place_dev, 0, wx, wy, def.hp, def.hp,
- def.needs_deposit ? on_deposit(wx, wy) : true });
+ Device dv{ place_dev, 0, wx, wy, def.hp, def.hp,
+ def.needs_deposit ? on_deposit(wx, wy) : true };
+ dv.graph_id = mount_at(wx, wy, place_dev, 0, def.hp);
+ devices.push_back(dv);
metal -= def.cost_metal;
energy -= def.cost_energy;
}
@@ -419,6 +435,14 @@ void App::run() {
graph.update_fire(FIXED_DT); // burn DoT + spread
graph.kill_grounded(); // destroy debris below the ground
+ // Devices mounted on the strut graph: if structural collapse (a
+ // snapped strut, kill_grounded) took out both mount nodes, the
+ // device falls too -- this is win-condition path (c), cutting
+ // supports to drop the reactor.
+ for (auto& dv : devices)
+ if (dv.graph_id >= 0 && !graph.is_device_alive(dv.graph_id))
+ dv.hp = 0.0f;
+
// Weapons: tick reloads.
for (auto& c : cannons)
if (c.cooldown > 0.0f) c.cooldown = std::max(0.0f, c.cooldown - FIXED_DT);
@@ -487,6 +511,8 @@ void App::run() {
}
accumulator -= FIXED_DT;
}
+ for (auto& dv : devices)
+ if (dv.hp <= 0.0f && dv.graph_id >= 0) graph.unmount_device(dv.graph_id);
devices.erase(std::remove_if(devices.begin(), devices.end(),
[](const Device& d){ return d.hp <= 0.0f; }), devices.end());
projectiles.erase(
diff --git a/src/game/build_graph.cpp b/src/game/build_graph.cpp
index a1195af..2398b9c 100644
--- a/src/game/build_graph.cpp
+++ b/src/game/build_graph.cpp
@@ -331,10 +331,25 @@ void BuildGraph::apply_splash(float x, float y, float radius, float damage,
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();
+ if (!brk.empty()) {
+ for (auto it = brk.rbegin(); it != brk.rend(); ++it)
+ edges.erase(edges.begin() + *it);
+ rebuild_topology();
+ }
+
+ // Splash also damages devices mounted within the radius.
+ for (auto& dv : devices) {
+ if (!dv.alive) continue;
+ float mx = 0.0f, my = 0.0f; int mounts = 0;
+ if (dv.node_a >= 0 && dv.node_a < (int)nodes.size())
+ { mx += nodes[dv.node_a].x; my += nodes[dv.node_a].y; mounts++; }
+ if (dv.node_b >= 0 && dv.node_b < (int)nodes.size())
+ { mx += nodes[dv.node_b].x; my += nodes[dv.node_b].y; mounts++; }
+ if (mounts == 0) continue;
+ mx /= mounts; my /= mounts;
+ float d = std::sqrt((mx-x)*(mx-x) + (my-y)*(my-y));
+ if (d < radius) { dv.hp -= damage * (1.0f - d / radius); if (dv.hp <= 0.0f) dv.alive = false; }
+ }
}
int BuildGraph::find_blocking_edge(float x, float y, float radius) const {
@@ -367,7 +382,17 @@ float BuildGraph::beam_fire(float ox, float oy, float dx, float dy, float range,
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
+ if (std::fabs(denom) < 1e-6f) {
+ // Ray parallel to segment. Check for collinear overlap.
+ float perp = std::fabs((ox - A.x) * ey - (oy - A.y) * ex);
+ if (perp > 1e-4f) continue; // parallel, no overlap
+ float ta = (A.x - ox) * dx + (A.y - oy) * dy;
+ float tb = (B.x - ox) * dx + (B.y - oy) * dy;
+ if (ta > tb) std::swap(ta, tb);
+ if (tb < 0.0f || ta > range) continue; // no overlap
+ crosses.push_back({std::max(0.0f, ta), i});
+ continue;
+ }
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});
@@ -395,6 +420,28 @@ float BuildGraph::beam_fire(float ox, float oy, float dx, float dy, float range,
edges.erase(edges.begin() + *it);
rebuild_topology();
}
+
+ // Beam also damages devices within threshold distance of its path.
+ {
+ float ex = hit_x - ox, ey = hit_y - oy;
+ float len2 = ex*ex + ey*ey;
+ if (len2 > 1e-6f) {
+ for (auto& dv : devices) {
+ if (!dv.alive) continue;
+ float mx = 0.0f, my = 0.0f; int mounts = 0;
+ if (dv.node_a >= 0 && dv.node_a < (int)nodes.size())
+ { mx += nodes[dv.node_a].x; my += nodes[dv.node_a].y; mounts++; }
+ if (dv.node_b >= 0 && dv.node_b < (int)nodes.size())
+ { mx += nodes[dv.node_b].x; my += nodes[dv.node_b].y; mounts++; }
+ if (mounts == 0) continue;
+ mx /= mounts; my /= mounts;
+ float t = std::clamp(((mx-ox)*ex + (my-oy)*ey) / len2, 0.0f, 1.0f);
+ float d = std::hypot(mx - (ox + t*ex), my - (oy + t*ey));
+ if (d < 0.6f) { dv.hp -= damage; if (dv.hp <= 0.0f) dv.alive = false; }
+ }
+ }
+ }
+
return stop;
}
@@ -439,6 +486,20 @@ void BuildGraph::update_fire(float dt) {
edges.erase(edges.begin() + *it);
rebuild_topology();
}
+
+ // Fire also damages devices mounted on burning struts.
+ for (int ei = 0; ei < (int)edges.size(); ei++) {
+ const auto& e = edges[ei];
+ if (!e.burning) continue;
+ float fd = materials[e.mat].burn_rate * dt;
+ for (auto& dv : devices) {
+ if (!dv.alive) continue;
+ if (dv.node_a == e.node_a || dv.node_a == e.node_b ||
+ dv.node_b == e.node_a || dv.node_b == e.node_b) {
+ dv.hp -= fd; if (dv.hp <= 0.0f) dv.alive = false;
+ }
+ }
+ }
}
void BuildGraph::break_edge(int edge_id) {
@@ -460,10 +521,97 @@ void BuildGraph::break_node(int node_id) {
if (e.node_a > node_id) e.node_a--;
if (e.node_b > node_id) e.node_b--;
}
+
+ // Fix up device mount indices: invalidate mounts on the removed node,
+ // shift indices past it, and mark devices dead when both mounts are gone.
+ for (auto& dv : devices) {
+ if (!dv.alive) continue;
+ if (dv.node_a == node_id) dv.node_a = -1;
+ if (dv.node_b == node_id) dv.node_b = -1;
+ if (dv.node_a > node_id) dv.node_a--;
+ if (dv.node_b > node_id) dv.node_b--;
+ if (dv.node_a < 0 && dv.node_b < 0) dv.alive = false;
+ }
+
rebuild_topology();
}
// =============================================================================
+// Devices mounted on the strut graph
+// =============================================================================
+
+int BuildGraph::mount_device(int na, int nb, int device_type, int team, float hp) {
+ if (na < 0 || na >= (int)nodes.size()) return -1;
+ if (nb < 0 || nb >= (int)nodes.size()) return -1;
+ if (na == nb) return -1;
+
+ MountedDevice md{};
+ md.node_a = na;
+ md.node_b = nb;
+ md.type = device_type;
+ md.team = team;
+ md.hp = hp;
+ md.max_hp = hp;
+ md.alive = true;
+ devices.push_back(md);
+ return (int)devices.size() - 1;
+}
+
+void BuildGraph::unmount_device(int device_id) {
+ if (device_id < 0 || device_id >= (int)devices.size()) return;
+ devices[device_id].alive = false;
+}
+
+bool BuildGraph::is_device_alive(int device_id) const {
+ if (device_id < 0 || device_id >= (int)devices.size()) return false;
+ return devices[device_id].alive && devices[device_id].hp > 0.0f;
+}
+
+float BuildGraph::get_device_hp(int device_id) const {
+ if (device_id < 0 || device_id >= (int)devices.size()) return 0.0f;
+ return devices[device_id].hp;
+}
+
+bool BuildGraph::damage_device(int device_id, float amount) {
+ if (device_id < 0 || device_id >= (int)devices.size()) return false;
+ auto& dv = devices[device_id];
+ if (!dv.alive || dv.hp <= 0.0f) return false;
+ dv.hp -= amount;
+ if (dv.hp <= 0.0f) {
+ dv.alive = false;
+ return true;
+ }
+ return false;
+}
+
+int BuildGraph::nearest_device(float x, float y, float radius) const {
+ int best = -1;
+ float best_d2 = radius * radius;
+ for (int i = 0; i < (int)devices.size(); i++) {
+ if (!devices[i].alive || devices[i].hp <= 0.0f) continue;
+ // Device position = midpoint of its valid mount nodes.
+ float mx = 0.0f, my = 0.0f;
+ int mounts = 0;
+ if (devices[i].node_a >= 0 && devices[i].node_a < (int)nodes.size()) {
+ mx += nodes[devices[i].node_a].x;
+ my += nodes[devices[i].node_a].y;
+ mounts++;
+ }
+ if (devices[i].node_b >= 0 && devices[i].node_b < (int)nodes.size()) {
+ mx += nodes[devices[i].node_b].x;
+ my += nodes[devices[i].node_b].y;
+ mounts++;
+ }
+ if (mounts == 0) continue;
+ mx /= mounts; my /= mounts;
+ float dx = mx - x, dy = my - y;
+ float d2 = dx*dx + dy*dy;
+ if (d2 < best_d2) { best_d2 = d2; best = i; }
+ }
+ return best;
+}
+
+// =============================================================================
// Queries
// =============================================================================
diff --git a/src/game/build_graph.hpp b/src/game/build_graph.hpp
index 4452139..cd8dc3d 100644
--- a/src/game/build_graph.hpp
+++ b/src/game/build_graph.hpp
@@ -67,6 +67,17 @@ struct BuildEdge {
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).
@@ -133,6 +144,24 @@ public:
// 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);
@@ -147,6 +176,7 @@ public:
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