362 lines
14 KiB
C++
362 lines
14 KiB
C++
#include "game/sim/movement.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include "game/sim/numeric.h"
|
|
|
|
namespace sots::sim {
|
|
|
|
namespace {
|
|
constexpr double kMinChordLength = 0.01;
|
|
|
|
// The epsilon the engine's vector normalise uses: below it the direction is zeroed.
|
|
constexpr double kNormaliseEpsilon = 1.1920928955078125e-07; // 2^-23, as a float32 literal
|
|
|
|
double Dot(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
|
Vec3 Sub(const Vec3& a, const Vec3& b) { return Vec3{a.x - b.x, a.y - b.y, a.z - b.z}; }
|
|
Vec3 Lerp(const Vec3& a, const Vec3& b, double f) {
|
|
return Vec3{a.x + (b.x - a.x) * f, a.y + (b.y - a.y) * f, a.z + (b.z - a.z) * f};
|
|
}
|
|
} // namespace
|
|
|
|
double Distance(const Vec3& a, const Vec3& b) {
|
|
const double dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
|
|
return std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
}
|
|
|
|
Vec3 AdvanceToward(const Vec3& pos, const Vec3& dest, double amount) {
|
|
const double d = Distance(pos, dest);
|
|
if (amount >= d || d <= 0) return dest;
|
|
return Lerp(pos, dest, amount / d);
|
|
}
|
|
|
|
double DistPointToSegment(const Vec3& p, const Vec3& a, const Vec3& b) {
|
|
const Vec3 ab = Sub(b, a);
|
|
const double len2 = Dot(ab, ab);
|
|
double u = 0.0;
|
|
if (len2 > 0) u = Clamp01(Dot(Sub(p, a), ab) / len2);
|
|
return Distance(p, Lerp(a, b, u));
|
|
}
|
|
|
|
bool IsGateTransitWaypoint(int waypointType) { return waypointType == 4 || waypointType == 5; }
|
|
bool IsNodeWaypoint(int waypointType) { return waypointType == 3; }
|
|
|
|
double StraightStep(double speed, double dt) { return F32(speed * dt); }
|
|
|
|
double NodeLineSpeed(double nodeSpeed, double distToLineSystem, const TuningTable& t) {
|
|
double ratio = 0.0;
|
|
if (t.STUTTER_SYSTEM_INFLUENCE_RADIUS > 0) {
|
|
ratio = distToLineSystem / t.STUTTER_SYSTEM_INFLUENCE_RADIUS;
|
|
}
|
|
return nodeSpeed * ((t.STUTTER_MAX_SPEED - t.STUTTER_MIN_SPEED) * ratio + t.STUTTER_MIN_SPEED);
|
|
}
|
|
|
|
bool SegmentSphereIntersect(const Vec3& from, const Vec3& to, const Vec3& centre, double radius,
|
|
double* tNear, double* tFar) {
|
|
constexpr double kFltMax = 3.4028234663852886e38;
|
|
const Vec3 d = Sub(to, from);
|
|
const double a = Dot(d, d);
|
|
if (a <= kNormaliseEpsilon) {
|
|
*tNear = *tFar = kFltMax;
|
|
return false;
|
|
}
|
|
const Vec3 fc = Sub(from, centre);
|
|
const double b = 2.0 * Dot(d, fc);
|
|
const double c = Dot(centre, centre) + Dot(from, from) - 2.0 * Dot(from, centre) - radius * radius;
|
|
const double disc = b * b - 4.0 * a * c;
|
|
double t0, t1;
|
|
if (std::fabs(disc) < kNormaliseEpsilon) {
|
|
t0 = t1 = -b / (2.0 * a);
|
|
if (!(t0 >= 0.0 && t0 <= 1.0)) {
|
|
*tNear = *tFar = kFltMax;
|
|
return false;
|
|
}
|
|
} else if (disc < 0) {
|
|
*tNear = *tFar = kFltMax;
|
|
return false;
|
|
} else {
|
|
const double root = std::sqrt(disc);
|
|
t0 = (-b - root) / (2.0 * a);
|
|
t1 = (-b + root) / (2.0 * a);
|
|
}
|
|
// Reject a sphere the segment does not reach, and report an open end as the sentinel
|
|
// the caller's clamp turns into the corresponding line end.
|
|
if (t1 < 0.0) {
|
|
*tNear = *tFar = -kFltMax;
|
|
return false;
|
|
}
|
|
if (t0 > 1.0) {
|
|
*tNear = *tFar = kFltMax;
|
|
return false;
|
|
}
|
|
*tNear = t0 < 0.0 ? -kFltMax : t0;
|
|
*tFar = t1 > 1.0 ? kFltMax : t1;
|
|
return true;
|
|
}
|
|
|
|
std::vector<StutterSegment> BuildStutterSegments(const Vec3& from, const Vec3& to,
|
|
const std::vector<Vec3>& systems,
|
|
const TuningTable& t) {
|
|
std::vector<StutterSegment> segs;
|
|
const double radius = t.STUTTER_SYSTEM_INFLUENCE_RADIUS;
|
|
const Vec3 d = Sub(to, from);
|
|
const double len = F32(std::sqrt(F32(Dot(d, d))));
|
|
if (len <= 0) return segs;
|
|
|
|
for (std::size_t i = 0; i < systems.size(); ++i) {
|
|
double t0 = 0, t1 = 0;
|
|
if (!SegmentSphereIntersect(from, to, systems[i], radius, &t0, &t1)) continue;
|
|
StutterSegment s;
|
|
s.start = ClampT(F32(t0 * len), 0.0, len);
|
|
s.end = ClampT(F32(t1 * len), 0.0, len);
|
|
s.systemIndex = static_cast<int>(i);
|
|
if (std::fabs(s.start - s.end) <= kMinStutterChord) continue;
|
|
segs.push_back(s);
|
|
}
|
|
|
|
std::sort(segs.begin(), segs.end(),
|
|
[](const StutterSegment& a, const StutterSegment& b) { return a.start < b.start; });
|
|
|
|
// The overlap pass, reproduced exactly -- see the header. One forward sweep over
|
|
// adjacent pairs, both boundaries set to the same value, nothing removed.
|
|
for (std::size_t i = 0; i + 1 < segs.size(); ++i) {
|
|
if (!(segs[i].end > segs[i + 1].start)) continue;
|
|
const double x = F32(segs[i].end + 0.5 * (segs[i].end - segs[i + 1].start));
|
|
segs[i].end = x;
|
|
segs[i + 1].start = x;
|
|
}
|
|
|
|
// Per-segment profile: closest approach of the (post-merge) chord to its system. The
|
|
// original computes this inside the step loop, after the merge, which is where the
|
|
// absence of a clamp on dist/radius can bite.
|
|
for (StutterSegment& s : segs) {
|
|
const Vec3 a = Lerp(from, to, s.start / len);
|
|
const Vec3 b = Lerp(from, to, s.end / len);
|
|
const double dist = F32(DistPointToSegment(systems[static_cast<std::size_t>(s.systemIndex)], a, b));
|
|
s.speedFactor = F32(NodeLineSpeed(1.0, dist, t));
|
|
}
|
|
return segs;
|
|
}
|
|
|
|
NodeLineStepResult NodeLineStep(double nodeSpeed, double dt, double lineLength,
|
|
const std::vector<StutterSegment>& segments) {
|
|
NodeLineStepResult r;
|
|
if (!(dt > 0.0)) return r; // dt <= 0: nothing moves, and `time < dt` is false
|
|
|
|
bool ranOut = false;
|
|
for (const StutterSegment& s : segments) {
|
|
if (s.start >= r.along) {
|
|
if (r.time >= dt) break;
|
|
const double tt = r.time + (s.start - r.along) / nodeSpeed; // plain speed in the gap
|
|
if (tt > dt) {
|
|
r.along = r.along + (dt - r.time) * nodeSpeed;
|
|
r.time = dt;
|
|
ranOut = true;
|
|
break;
|
|
}
|
|
r.time = tt;
|
|
r.along = s.start;
|
|
}
|
|
const double v = F32(s.speedFactor * nodeSpeed);
|
|
if (s.end < r.along) continue; // swallowed or inverted by the overlap pass
|
|
if (dt <= r.time) break;
|
|
const double tt = r.time + (s.end - r.along) / v;
|
|
if (tt > dt) {
|
|
r.along = r.along + (dt - r.time) * v;
|
|
r.time = dt;
|
|
ranOut = true;
|
|
break;
|
|
}
|
|
r.time = tt;
|
|
r.along = s.end;
|
|
if (r.time >= dt) break;
|
|
}
|
|
|
|
// Segments exhausted with time to spare: run straight to the end of the line.
|
|
if (!ranOut && r.time < dt) {
|
|
if (!(lineLength < r.along) && dt > r.time) {
|
|
const double tt = r.time + (lineLength - r.along) / nodeSpeed;
|
|
if (tt <= dt) {
|
|
r.time = tt;
|
|
r.along = lineLength;
|
|
} else {
|
|
r.along += nodeSpeed * (dt - r.time);
|
|
r.time = dt;
|
|
}
|
|
}
|
|
}
|
|
|
|
r.arrived = r.time < dt || std::fabs(r.along - lineLength) < kNormaliseEpsilon;
|
|
return r;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Range, clamping and arrival
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
double FleetMinShipRange(const std::vector<double>& shipRanges, double bias) {
|
|
// The accumulator starts at FLT_MAX, so an empty fleet is unconstrained rather than
|
|
// stranded -- worth knowing, because the stranded test compares against exactly 0.
|
|
double best = static_cast<double>(std::numeric_limits<float>::max());
|
|
for (double r : shipRanges) best = std::min(best, r);
|
|
return F32(best + bias);
|
|
}
|
|
|
|
MoveStepResult ResolveMoveStep(double step, double minShipRange, double distance) {
|
|
MoveStepResult r;
|
|
double range = F32(minShipRange + kRangeGrace);
|
|
if (distance > range) {
|
|
// The original asks for the minimum range a second time, with no grace margin, and
|
|
// takes the whole budget away when it is exactly zero. It zeroes the *range*, not
|
|
// the step: the step is still the divisor of the pass fraction.
|
|
if (minShipRange == 0.0) {
|
|
range = 0.0;
|
|
r.stranded = true;
|
|
}
|
|
}
|
|
const double move = std::min(std::min(range, step), distance);
|
|
r.range = range;
|
|
r.moved = move; // deliberately not floored at 0
|
|
r.arrived = move == distance;
|
|
return r;
|
|
}
|
|
|
|
Vec3 AdvanceAlongDirection(const Vec3& pos, const Vec3& dest, double move) {
|
|
const Vec3 delta = Sub(dest, pos);
|
|
const double len = std::sqrt(Dot(delta, delta));
|
|
if (!(len > kNormaliseEpsilon)) return pos; // the direction is zeroed; nothing moves
|
|
const Vec3 dir{delta.x / len, delta.y / len, delta.z / len};
|
|
return Vec3{F32(pos.x + F32(dir.x * move)), F32(pos.y + F32(dir.y * move)),
|
|
F32(pos.z + F32(dir.z * move))};
|
|
}
|
|
|
|
double ConsumeShipRange(double shipRange, double moved, bool exempt) {
|
|
if (exempt) return shipRange;
|
|
const double v = F32(shipRange - moved);
|
|
return v < 0.0 ? 0.0 : v;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Pass fraction
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
double PassFraction(int waypointType, double distance, double step) {
|
|
if (!IsNodeWaypoint(waypointType)) return 1.0;
|
|
if (step == 0) return 1.0; // the clamp of an infinity/NaN quotient lands at the bound
|
|
return Clamp01(F32(distance / step));
|
|
}
|
|
|
|
double BlockedPassFraction(double moved, double step) {
|
|
if (step == 0) return 1.0;
|
|
return Clamp01(F32(moved / step));
|
|
}
|
|
|
|
double RemainingPassTime(double fraction, double dt) {
|
|
if (fraction < kPassCompleteFraction) return F32((1.0 - fraction) * dt);
|
|
return 0.0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Probabilistic jump
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
JumpResult RollProbabilisticJump(double castEfficiency, double castThreshold, IRandom& rng) {
|
|
JumpResult r;
|
|
r.roll = rng.NextFloat();
|
|
r.draws = 1;
|
|
// The efficiency is read as a float32 and the product is stored back to a float32 slot
|
|
// before the comparison, so both narrowings are real.
|
|
const double v = F32(static_cast<double>(r.roll) * F32(castEfficiency));
|
|
if (v > castThreshold) {
|
|
r.arrived = false;
|
|
r.scatter = v;
|
|
rng.NextUInt32(); // the random direction the miss is scattered along
|
|
r.draws = 2;
|
|
} else {
|
|
r.arrived = true;
|
|
r.scatter = 0;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// The turn's pass schedule
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
std::vector<MovementPass> PlanFleetMovement(const std::vector<FleetMovementEntry>& fleets) {
|
|
std::set<int> pursuers, prey, followers, resolved;
|
|
std::map<int, int> preyOf;
|
|
for (const FleetMovementEntry& f : fleets) {
|
|
if (f.targetFleetId == 0) continue;
|
|
if (f.relation != 0) {
|
|
followers.insert(f.fleetId);
|
|
} else {
|
|
pursuers.insert(f.fleetId);
|
|
prey.insert(f.targetFleetId);
|
|
preyOf[f.fleetId] = f.targetFleetId;
|
|
}
|
|
}
|
|
|
|
std::vector<MovementPass> out;
|
|
// Pass 1: the prey move half a turn.
|
|
for (int id : prey) out.push_back({id, kHalfStep, 1});
|
|
|
|
// Pass 2: every pursuer (of the *original* set) re-validates and moves half a turn. A
|
|
// pursuer that arrives retires itself and its prey from the rest of the schedule.
|
|
const std::set<int> pursuersAtPass2 = pursuers;
|
|
for (int id : pursuersAtPass2) {
|
|
out.push_back({id, kHalfStep, 2});
|
|
const FleetMovementEntry* e = nullptr;
|
|
for (const FleetMovementEntry& f : fleets)
|
|
if (f.fleetId == id) e = &f;
|
|
if (!e || !e->caught) continue;
|
|
const int target = preyOf[id];
|
|
resolved.insert(id);
|
|
resolved.insert(target);
|
|
pursuers.erase(id);
|
|
prey.erase(target);
|
|
followers.erase(target);
|
|
}
|
|
|
|
// Pass 3: the prey that got away take their second half turn.
|
|
for (int id : prey) out.push_back({id, kHalfStep, 3});
|
|
|
|
// Pass 4: everyone else, in fleet order. A pursuer whose chase failed is still in the
|
|
// pursuer set and gets a half turn; anything already scheduled is skipped.
|
|
for (const FleetMovementEntry& f : fleets) {
|
|
const int id = f.fleetId;
|
|
if (resolved.count(id)) continue;
|
|
if (pursuers.count(id)) {
|
|
out.push_back({id, kHalfStep, 4});
|
|
continue;
|
|
}
|
|
if (prey.count(id)) continue;
|
|
if (followers.count(id)) continue;
|
|
out.push_back({id, kFullStep, 4});
|
|
}
|
|
|
|
// Pass 5: the followers take a full turn.
|
|
for (int id : followers) out.push_back({id, kFullStep, 5});
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Gate traffic
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
std::vector<int> GateTrafficTotals(const std::vector<GateTrafficEntry>& fleets, int playerCount) {
|
|
std::vector<int> totals(static_cast<std::size_t>(std::max(playerCount, 0)), 0);
|
|
for (const GateTrafficEntry& f : fleets) {
|
|
if (f.waypointType < 0) continue; // no waypoints: the fleet is not in transit
|
|
if (!IsGateTransitWaypoint(f.waypointType)) continue;
|
|
if (f.ownerIndex < 0 || f.ownerIndex >= static_cast<int>(totals.size())) continue;
|
|
totals[static_cast<std::size_t>(f.ownerIndex)] += f.traffic;
|
|
}
|
|
return totals;
|
|
}
|
|
|
|
} // namespace sots::sim
|