6#include <unordered_set>
10Result<void> groupFailure(
const char*
message) {
14double remainingRoute(Unit&
unit,
const OrderRecord&
order) {
15 auto motion =
unit.motion();
16 auto navigation =
unit.navigation();
17 WorldPosition
previous{motion->x, motion->y};
18 double remaining = 0.0;
19 const auto add = [&](WorldPosition
next) {
23 if (navigation->plannedOrderId ==
order.id && !navigation->unreachable) {
24 for (std::size_t i = navigation->waypointIndex; i < navigation->waypoints.size(); ++i)
25 add(navigation->waypoints[i]);
35 groupFailure(
"Movement group requires two units and positive finite lead distance").
status());
39 group.state.members.reserve(batch.
units.size());
42 if (!
unit || !
unit->durability()->alive ||
unit->containment()->container.isBound())
44 groupFailure(
"Movement group member must be a live uncontained owned unit").
status());
45 group.handles.push_back(ecs::handle_of(
unit));
46 group.epochs.push_back(
unit->orders()->values.membershipEpoch_);
49 movementGroups_.reserve(movementGroups_.size() + 1);
54 if (!admitted)
return admitted;
55 for (std::size_t i = 0; i <
group.state.members.size(); ++i)
56 group.state.members[i].orderId = admitted.value().orderIds[i];
57 movementGroups_.push_back(std::move(
group));
65 auto view = ecs::View<Unit, Unit::Navigation>();
66 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
67 auto [navigation] = *it;
68 navigation->formationSpeedFactor = 1.f;
69 navigation->formationTarget.reset();
72 std::size_t processed = 0;
73 for (
auto&
group : movementGroups_) {
75 active.state.leadDistance =
group.state.leadDistance;
76 std::vector<double> distances;
77 std::vector<WorldPosition> targets;
79 bool openTravel =
true;
80 double furthest = 0.0;
81 for (std::size_t i = 0; i <
group.handles.size(); ++i) {
82 auto*
unit =
dynamic_cast<Unit*
>(ecs::try_get(
group.handles[i]));
83 if (!
unit || !
unit->durability()->alive ||
unit->containment()->container.isBound() ||
84 unit->orders()->values.membershipEpoch_ !=
group.epochs[i])
91 auto navigation =
unit->navigation();
92 if (pathfinder_ && navigation->plannedOrderId ==
current.value()->id && !navigation->unreachable &&
93 navigation->waypointIndex + 1 < navigation->waypoints.size()) {
95 const auto waypoint = navigation->waypoints[navigation->waypointIndex];
96 const double tolerance =
97 std::max(
static_cast<double>(
unit->motion()->arrivalRadius),
98 static_cast<double>(
unit->crowd()->radius) + navigationGrid_.
cellSize * 0.1);
99 if (std::hypot(
static_cast<double>(
position.x) - waypoint.x,
100 static_cast<double>(
position.y) - waypoint.y) <= tolerance &&
102 navigation->waypoints[navigation->waypointIndex + 1],
103 unit->crowd()->radius))
104 ++navigation->waypointIndex;
106 const double remaining = remainingRoute(*
unit, *
current.value());
107 if (!std::isfinite(remaining))
109 groupFailure(
"Movement group route distance must be finite").
status());
112 const bool yielding =
unit->navigation()->trafficWaiting ||
unit->supply()->convoyWaiting ||
113 (
unit->morale()->retreating &&
unit->tactics()->retreatCovering);
114 if (!yielding) furthest = std::max(furthest, remaining);
115 openTravel = openTravel && !yielding;
116 targets.push_back(
current.value()->target);
119 distances.push_back(remaining);
122 active.state.members.push_back(
group.state.members[i]);
125 if (
group.handles.size() < 2)
continue;
129 const double residual = offsetLength > 0.0 ? std::max(0.0, 1.0 -
group.state.leadDistance / offsetLength) : 0.0;
130 std::vector<WorldPosition> movingTargets;
131 movingTargets.reserve(targets.size());
132 for (std::size_t i = 0; i < targets.size(); ++i) {
133 const WorldPosition
target{
static_cast<float>(targets[i].x +
offsetX * residual),
134 static_cast<float>(targets[i].
y +
offsetY * residual)};
137 movingTargets.push_back(
target);
138 if (pathfinder_ && openTravel) {
139 auto*
unit =
static_cast<Unit*
>(ecs::try_get(
group.handles[i]));
140 const WorldPosition
from{
unit->motion()->x,
unit->motion()->y};
142 unit->crowd()->radius) &&
144 unit->crowd()->radius);
147 for (std::size_t i = 0; i <
group.handles.size(); ++i) {
148 auto*
unit =
static_cast<Unit*
>(ecs::try_get(
group.handles[i]));
149 const double excess =
150 pathfinder_ && !openTravel ? 0.0 : std::max(0.0, furthest - distances[i] -
group.state.leadDistance);
151 unit->navigation()->formationSpeedFactor =
152 static_cast<float>(std::clamp(1.0 - excess /
group.state.leadDistance, 0.0, 1.0));
157 unit->navigation()->formationTarget = movingTargets[i];
160 if (pathfinder_)
unit->navigation()->waypointIndex =
unit->navigation()->waypoints.size();
165 std::erase_if(movementGroups_, [](
const auto&
group) {
return group.handles.size() < 2; });
169Result<std::vector<RTSMovementGroupSnapshot>> RTS::captureMovementGroups()
const {
170 std::vector<RTSMovementGroupSnapshot> result;
171 for (
const auto&
group : movementGroups_) {
172 RTSMovementGroupSnapshot
active;
174 for (std::size_t i = 0; i <
group.handles.size(); ++i) {
175 auto*
unit =
dynamic_cast<Unit*
>(ecs::try_get(
group.handles[i]));
176 if (!
unit || !
unit->durability()->alive ||
unit->containment()->container.isBound() ||
177 unit->orders()->values.membershipEpoch_ !=
group.epochs[i])
180 if (!
current)
return Result<std::vector<RTSMovementGroupSnapshot>>::failure(
current.status());
185 if (
active.members.size() >= 2) {
187 return left.subject.format() < right.subject.format();
189 result.push_back(std::move(
active));
192 std::sort(result.begin(), result.end(), [](
const auto&
left,
const auto&
right) {
193 return left.members.front().subject.format() < right.members.front().subject.format();
195 return Result<std::vector<RTSMovementGroupSnapshot>>::success(std::move(result));
198Result<std::vector<RTS::MovementGroup>> RTS::prepareMovementGroups(
const RTSStateSnapshot& snapshot)
const {
199 using Output = Result<std::vector<MovementGroup>>;
200 if (snapshot.version == 1 && !snapshot.movementGroups.empty())
201 return Output::failure(groupFailure(
"Version 1 RTS snapshots cannot contain movement groups").
status());
202 std::vector<MovementGroup> result;
203 std::unordered_set<SubjectRef> claimed;
204 for (
const auto& state : snapshot.movementGroups) {
205 if (
state.members.size() < 2 || !std::isfinite(
state.leadDistance) ||
state.leadDistance <= 0.f)
206 return Output::failure(groupFailure(
"Invalid movement-group snapshot geometry").
status());
211 const auto saved = std::find_if(snapshot.units.begin(), snapshot.units.end(),
212 [&](
const auto& candidate) { return candidate.subject == member.subject; });
213 if (!
unit || saved == snapshot.units.end() || !claimed.insert(
member.subject).second ||
214 !saved->durability.alive || saved->container.isValid())
215 return Output::failure(groupFailure(
"Invalid or duplicate movement-group member").
status());
216 OrderComponent validator;
217 auto restored = validator.restoreState(saved->orders);
218 if (!restored)
return Output::failure(restored.status());
219 auto current = validator.current();
221 return Output::failure(
222 groupFailure(
"Movement-group membership does not match its active order").
status());
223 group.handles.push_back(ecs::handle_of(
unit));
224 group.epochs.push_back(
unit->orders()->values.membershipEpoch_);
226 result.push_back(std::move(
group));
228 return Output::success(std::move(result));
building::EdgeCurveGroup group
wgpu::PopErrorScopeStatus status
std::array< float, 3 > position
graphics::Canvas * previous
RTS module owner and phase-one composition profile entry point.
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Result< FanOutReceipt > fanOut(std::span< const ecs::EntityHandle > unitHandles, const CommandSpec &command, const FormationSpec &formation)
Submit one command to every selected live Unit.
Unit * findUnit(SubjectRef subject) const noexcept
Resolve a module-owned unit by persistent subject identity.
Result< FanOutReceipt > submitMovementGroup(const MovementGroupBatch &batch)
Commit a caller-built immediate Move group with coordinated pacing.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Result< std::optional< OrderRecord > > readCurrent(OrderComponent &orders)
bool isFinitePosition(WorldPosition position)
EVENGINE_API_DOMAINS bool isFormationSegmentClear(const map::Pathfinder &pathfinder, const NavigationGrid &grid, WorldPosition from, WorldPosition to, float radius)
One domain-neutral command submitted to a unit's generic order queue.
Caller-built admission batch for an immediate coordinated Move command. @cost Linear in unit count; c...
float leadDistance
Positive world-space allowance before leaders slow for lagging members.
std::vector< SubjectRef > units
Owned stable identities; no entity pointers are retained.