载入中...
搜索中...
未找到
HexUnits.cpp
浏览该文件的文档.
1#include "hexmap/HexUnits.h"
2
3#include "common/Diagnostic.h"
4
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <numbers>
9#include <string>
10#include <utility>
11#include <vector>
12
13namespace eve::hexmap {
14namespace {
15
22[[nodiscard]] Diagnostic invalidArgument(std::string message) {
24}
25
27[[nodiscard]] Diagnostic notFound(std::string message) {
29}
30
32[[nodiscard]] float wrapDegrees(float degrees) noexcept {
33 if (!std::isfinite(degrees)) return 0.f;
34 float wrapped = std::fmod(degrees + 180.f, 360.f);
35 if (wrapped < 0.f) wrapped += 360.f;
36 return wrapped - 180.f;
37}
38
40[[nodiscard]] float angularDifference(float from, float to) noexcept { return wrapDegrees(to - from); }
41
43[[nodiscard]] float yawFromDirection(HexVec3 direction) noexcept {
44 return std::atan2(direction.x, direction.z) * (180.f / std::numbers::pi_v<float>);
45}
46
48[[nodiscard]] HexVec3 bezierPoint(HexVec3 a, HexVec3 b, HexVec3 c, float t) noexcept {
49 const float r = 1.f - t;
50 return a * (r * r) + b * (2.f * r * t) + c * (t * t);
51}
52
54[[nodiscard]] HexVec3 bezierDerivative(HexVec3 a, HexVec3 b, HexVec3 c, float t) noexcept {
55 const HexVec3 slope = (b - a) * (1.f - t) + (c - b) * t;
56 return HexVec3{2.f * slope.x, 0.f, 2.f * slope.z};
57}
58
60struct TravelSegment {
62 HexCoordinates from{};
64 HexCoordinates to{};
66 HexVec3 a{};
68 HexVec3 b{};
70 HexVec3 c{};
71};
72
85[[nodiscard]] TravelSegment travelSegment(const HexMap& map, const std::vector<std::int32_t>& path,
86 std::int32_t segment) noexcept {
87 const std::int32_t last = static_cast<std::int32_t>(path.size()) - 1;
88 const std::int32_t index = segment < 1 ? 1 : (segment > last ? last : segment);
89
90 TravelSegment result;
91 result.from = map.coordinatesAt(path[static_cast<std::size_t>(index - 1)]);
92 result.to = map.coordinatesAt(path[static_cast<std::size_t>(index)]);
93 result.b = map.cellPosition(result.from);
94 result.c = (result.b + map.cellPosition(result.to)) * 0.5f;
95 if (index <= 1) {
96 result.a = result.b;
97 } else {
98 result.a = (map.cellPosition(map.coordinatesAt(path[static_cast<std::size_t>(index - 2)])) + result.b) * 0.5f;
99 }
100 return result;
101}
102
103} // namespace
104
105// --- lookup -----------------------------------------------------------------
106
107HexUnitRegistry::Unit* HexUnitRegistry::find(std::int32_t unitId) noexcept {
108 if (unitId < 0 || unitId >= static_cast<std::int32_t>(units_.size())) return nullptr;
109 return &units_[static_cast<std::size_t>(unitId)];
110}
111
112const HexUnitRegistry::Unit* HexUnitRegistry::find(std::int32_t unitId) const noexcept {
113 if (unitId < 0 || unitId >= static_cast<std::int32_t>(units_.size())) return nullptr;
114 return &units_[static_cast<std::size_t>(unitId)];
115}
116
117std::int32_t HexUnitRegistry::unitIdAt(HexCoordinates coordinates) const noexcept {
118 // Ids are positional, and `Unit::location` caches the occupied coordinates so
119 // this lookup needs no map. A detached unit (`locationIndex < 0`, e.g. after the
120 // map shrank under it) must not answer for a cell: its cached coordinates are a
121 // leftover, and reporting it made it occupy - and block - whatever cell that
122 // leftover happened to name.
123 for (std::size_t index = 0; index < units_.size(); ++index) {
124 if (units_[index].locationIndex < 0) continue;
125 if (units_[index].location == coordinates) return static_cast<std::int32_t>(index);
126 }
127 return -1;
128}
129
131 // The returned callable borrows `*this` and reads the cached occupied cells, so
132 // it stays valid exactly as long as this registry does; see the ownership note
133 // on `HexUnitRegistry::occupancyQuery`.
134 return HexOccupancyQuery{[this](HexCoordinates coordinates) { return isOccupied(coordinates); }};
135}
136
137// --- adding and removing ----------------------------------------------------
138
140 HexCoordinates location, float orientation) {
141 if (map.empty()) return Result<std::int32_t>::failure(invalidArgument("cannot add a unit to an empty map"));
142 if (!map.contains(location))
143 return Result<std::int32_t>::failure(invalidArgument("unit location is outside the hex map"));
144 if (isOccupied(location))
145 return Result<std::int32_t>::failure(invalidArgument("unit location is already occupied"));
146
147 Unit unit;
148 unit.locationIndex = map.indexOf(location);
149 unit.location = location;
150 unit.orientation = orientation;
151 unit.visionIndex = unit.locationIndex;
152 units_.push_back(std::move(unit));
153
154 // The id is the unit's position in the registry, so it stays valid until a
155 // removal shifts the ids that follow it.
156 const std::int32_t id = static_cast<std::int32_t>(units_.size()) - 1;
157 auto granted = visibility.increase(map, scratch, location, tuning_.visionRange);
158 if (!granted.ok()) {
159 units_.pop_back();
160 return Result<std::int32_t>::failure(granted.status());
161 }
163}
164
166 std::int32_t unitId) {
167 const Unit* unit = find(unitId);
168 if (unit == nullptr) return Result<void>::failure(notFound("unknown unit id"));
169
170 // Vision is always granted from `visionIndex`; while a unit travels that is the
171 // cell it walks through, not the destination cell `locationIndex` reserves.
172 // A detached unit (`visionIndex < 0`) holds no counter, so there is nothing to
173 // withdraw - `coordinatesAt(-1)` would have named cell (0, 0) and corrupted
174 // another viewer's counter there.
175 if (unit->visionIndex >= 0) {
176 const HexCoordinates viewer = map.coordinatesAt(unit->visionIndex);
177 auto withdrawn = visibility.decrease(map, scratch, viewer, tuning_.visionRange);
178 if (!withdrawn.ok()) return Result<void>::failure(withdrawn.status());
179 }
180
181 // Ids are positional: every unit after `unitId` shifts down by one, so callers
182 // must re-read `sample`/`snapshot` instead of reusing ids captured earlier.
183 units_.erase(units_.begin() + static_cast<std::ptrdiff_t>(unitId));
184 return Result<void>::success();
185}
186
187void HexUnitRegistry::removeAll(HexMap& map, HexVisibility& visibility) noexcept {
188 units_.clear();
189 visibility.clear(map);
190}
191
192// --- snapshots ---------------------------------------------------------------
193
194Result<HexUnitSample> HexUnitRegistry::makeSample(const HexMap& map, std::int32_t unitId, const Unit& unit) const {
195 HexUnitSample sample;
196 sample.id = unitId;
197 sample.location = unit.location;
198 sample.orientation = unit.orientation;
199 sample.traveling = unit.traveling;
200
201 if (unit.locationIndex < 0 || unit.locationIndex >= map.cellCount()) {
202 // The unit's cell is no longer part of the map; `refreshPositions` is what
203 // takes such a unit off the grid. Until then its position stays at origin.
204 return Result<HexUnitSample>::success(sample);
205 }
206 if (!unit.traveling || unit.path.size() < 2) {
207 sample.position = map.cellPosition(unit.location);
208 return Result<HexUnitSample>::success(sample);
209 }
210
211 const TravelSegment segment = travelSegment(map, unit.path, unit.segment);
212 sample.travelFrom = segment.from;
213 sample.position = bezierPoint(segment.a, segment.b, segment.c, unit.t);
214 return Result<HexUnitSample>::success(sample);
215}
216
217// --- travel ------------------------------------------------------------------
218
220 std::int32_t unitId, const std::vector<std::int32_t>& path) {
221 Unit* unit = find(unitId);
222 if (unit == nullptr) return Result<void>::failure(invalidArgument("unknown unit id"));
223
224 if (path.empty()) return Result<void>::failure(invalidArgument("travel path must not be empty"));
225 if (path.front() != unit->locationIndex)
226 return Result<void>::failure(invalidArgument("travel path must start on the unit's cell"));
227
228 const std::int32_t cellCount = map.cellCount();
229 for (std::size_t index = 0; index < path.size(); ++index) {
230 if (path[index] < 0 || path[index] >= cellCount)
231 return Result<void>::failure(invalidArgument("travel path leaves the hex map"));
232 if (index > 0 && map.coordinatesAt(path[index - 1]).distanceTo(map.coordinatesAt(path[index])) != 1)
233 return Result<void>::failure(invalidArgument("travel path steps between non-adjacent cells"));
234 }
235
236 const std::int32_t destinationIndex = path.back();
237 const HexCoordinates destination = map.coordinatesAt(destinationIndex);
238 // The unit reserves the destination itself, so the reachability test must run
239 // without this registry's own occupancy predicate.
240 if (!isValidDestination(map, destination, {}))
241 return Result<void>::failure(invalidArgument("travel destination is not a valid destination"));
242
243 if (path.size() <= 1) return Result<void>::success();
244
245 // Withdraw from the cell that actually holds this unit's vision. While travelling
246 // that is `visionIndex` (the cell the unit has reached), *not* `location`, which
247 // already reserves the destination: withdrawing there decremented a counter the
248 // unit never raised and left the corridor it was walking through permanently lit,
249 // including after the plan was replaced by this very call.
250 const std::int32_t viewerIndex = unit->visionIndex >= 0 ? unit->visionIndex : unit->locationIndex;
251 const HexCoordinates origin = map.coordinatesAt(viewerIndex);
252 const HexCoordinates firstStep = map.coordinatesAt(path[1]);
253
254 // Visibility moves before the unit does, so a failed sweep leaves the unit
255 // exactly where it was instead of half-way into a travel plan.
256 auto withdrawn = visibility.decrease(map, scratch, origin, tuning_.visionRange);
257 if (!withdrawn.ok()) return Result<void>::failure(withdrawn.status());
258 auto granted = visibility.increase(map, scratch, firstStep, tuning_.visionRange);
259 if (!granted.ok()) {
260 visibility.increase(map, scratch, origin, tuning_.visionRange)
261 .ignore("restore the origin viewer after a failed travel start");
262 return Result<void>::failure(granted.status());
263 }
264
265 unit->path.assign(path.begin(), path.end());
266 unit->segment = 1;
267 unit->t = 0.f;
268 unit->traveling = true;
269 unit->locationIndex = destinationIndex;
270 unit->location = destination;
271 unit->visionIndex = path[1];
272 return Result<void>::success();
273}
274
276 std::int32_t unitId, float dt) {
277 Unit* unit = find(unitId);
278 if (unit == nullptr) return Result<HexUnitSample>::failure(notFound("unknown unit id"));
279 if (!unit->traveling) return makeSample(map, unitId, *unit);
280
281 const float step = dt > 0.f ? dt : 0.f;
282 unit->t += step * tuning_.travelSpeed;
283
284 const std::int32_t lastSegment = static_cast<std::int32_t>(unit->path.size()) - 1;
285 while (unit->t >= 1.f && unit->segment < lastSegment) {
286 unit->t -= 1.f;
287 ++unit->segment;
288 const HexCoordinates left = map.coordinatesAt(unit->path[static_cast<std::size_t>(unit->segment - 1)]);
289 const HexCoordinates right = map.coordinatesAt(unit->path[static_cast<std::size_t>(unit->segment)]);
290 auto withdrawn = visibility.decrease(map, scratch, left, tuning_.visionRange);
291 if (!withdrawn.ok()) return Result<HexUnitSample>::failure(withdrawn.status());
292 auto granted = visibility.increase(map, scratch, right, tuning_.visionRange);
293 if (!granted.ok()) return Result<HexUnitSample>::failure(granted.status());
294 unit->visionIndex = unit->path[static_cast<std::size_t>(unit->segment)];
295 }
296 if (unit->t >= 1.f) {
297 // The final segment is consumed: the unit stands on its reserved destination.
298 unit->traveling = false;
299 unit->t = 0.f;
300 unit->segment = 1;
301 unit->visionIndex = unit->locationIndex;
302 unit->path.clear();
303 return makeSample(map, unitId, *unit);
304 }
305
306 if (dt > 0.f) {
307 const TravelSegment segment = travelSegment(map, unit->path, unit->segment);
308 const HexVec3 velocity = bezierDerivative(segment.a, segment.b, segment.c, unit->t);
309 if (velocity.x * velocity.x + velocity.z * velocity.z > 1e-8f) {
310 const float heading = yawFromDirection(velocity);
311 const float turn = tuning_.rotationSpeed * dt;
312 const float delta = angularDifference(unit->orientation, heading);
313 const float applied = delta > turn ? turn : (delta < -turn ? -turn : delta);
314 unit->orientation = wrapDegrees(unit->orientation + applied);
315 }
316 }
317 return makeSample(map, unitId, *unit);
318}
319
320void HexUnitRegistry::advanceAll(HexMap& map, HexVisibility& visibility, HexSearchContext& scratch, float dt) {
321 const std::int32_t count = unitCount();
322 for (std::int32_t unitId = 0; unitId < count; ++unitId) {
323 // Every cell a unit can step onto was validated by `beginTravel`, so the
324 // per-unit vision sweeps cannot fail here; `advance` reports a failure
325 // rather than continuing past it.
326 advance(map, visibility, scratch, unitId, dt).ignore("travel cells are validated by beginTravel");
327 }
328}
329
330// --- bulk state --------------------------------------------------------------
331
333 visibility.clear(map);
334 for (const Unit& unit : units_) {
335 // A detached unit holds no counter, and `coordinatesAt(-1)` would resolve to
336 // cell (0, 0) and grant a viewer there that no unit owns.
337 if (unit.visionIndex < 0) continue;
338 // The viewer cell is `visionIndex`, the cell whose fog of war this unit
339 // currently holds; an idle unit's `visionIndex` is its occupied cell.
340 visibility.increase(map, scratch, map.coordinatesAt(unit.visionIndex), tuning_.visionRange)
341 .ignore("unit cells are validated when a unit is added or restored");
342 }
343}
344
345void HexUnitRegistry::refreshPositions(const HexMap& map) noexcept {
346 // Positions are derived from the map on demand, so an idle unit picks up a new
347 // elevation or perturbation without any write here. What this pass does check
348 // is that the cached cell still exists: a unit outside the grid is taken off
349 // the grid so `unitIdAt` stops reporting it.
350 const std::int32_t cellCount = map.cellCount();
351 for (Unit& unit : units_) {
352 if (unit.locationIndex >= 0 && unit.locationIndex < cellCount) continue;
353 unit.locationIndex = -1;
354 unit.location = HexCoordinates{};
355 unit.visionIndex = -1;
356 unit.traveling = false;
357 unit.segment = 1;
358 unit.t = 0.f;
359 unit.path.clear();
360 }
361}
362
363std::vector<HexUnitState> HexUnitRegistry::snapshot() const {
364 std::vector<HexUnitState> states;
365 states.reserve(units_.size());
366 for (const Unit& unit : units_) {
368 state.locationIndex = unit.locationIndex;
369 state.orientation = unit.orientation;
370 states.push_back(state);
371 }
372 return states;
373}
374
376 const std::vector<HexUnitState>& states) {
377 // The registry is cleared before validation, which is why a rejected payload
378 // leaves it empty instead of partially populated: there is no half-restored
379 // registry any caller could observe. The map's explored latches stay latched
380 // because those are one-way by design.
381 units_.clear();
382 visibility.clear(map);
383
384 const std::int32_t cellCount = map.cellCount();
385 std::vector<std::int32_t> claimed;
386 claimed.reserve(states.size());
387 for (const HexUnitState& state : states) {
388 if (state.locationIndex < 0 || state.locationIndex >= cellCount)
389 return Result<void>::failure(invalidArgument("restored unit cell is outside the hex map"));
390 if (std::find(claimed.begin(), claimed.end(), state.locationIndex) != claimed.end())
391 return Result<void>::failure(invalidArgument("restored units occupy the same cell"));
392 if (!isValidDestination(map, map.coordinatesAt(state.locationIndex), {}))
393 return Result<void>::failure(invalidArgument("restored unit cell is not a valid destination"));
394 claimed.push_back(state.locationIndex);
395 }
396
397 units_.reserve(states.size());
398 for (const HexUnitState& state : states) {
399 Unit unit;
400 unit.locationIndex = state.locationIndex;
401 unit.location = map.coordinatesAt(state.locationIndex);
402 unit.orientation = state.orientation;
403 unit.visionIndex = state.locationIndex;
404 units_.push_back(std::move(unit));
405 }
406 refreshVisibility(map, visibility, scratch);
407 return Result<void>::success();
408}
409
410} // namespace eve::hexmap
std::string from
float degrees
Definition CardTypes.cpp:35
Stable, structured diagnostics shared by engine modules.
std::string message
double r
HexVec3 left
HexVec3 right
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
Units that occupy cells, travel along paths and carry vision.
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string path
Definition PlayHost.cpp:110
float t
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
std::uint32_t count
TacticalUnit * unit
TacticalUnit::TurnResources turn
float step
Definition TreeMesh.cpp:314
V3 heading
Definition TreeMesh.cpp:292
uint32_t index
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.
Definition Diagnostic.h:125
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
An editable, chunked, pointy-top hex map.
Definition HexMap.h:72
bool empty() const noexcept
Whether the map holds any cell.
Definition HexMap.h:92
HexCoordinates coordinatesAt(std::int32_t index) const noexcept
Coordinates of a linear cell index; out-of-range indices return (0, 0).
Definition HexMap.cpp:69
std::int32_t indexOf(HexCoordinates coordinates) const noexcept
Linear cell index of coordinates (offset order), or -1 when outside the grid.
Definition HexMap.cpp:64
HexVec3 cellPosition(HexCoordinates coordinates) const noexcept
Cell centre including elevation and vertical perturbation.
Definition HexMap.cpp:170
bool contains(HexCoordinates coordinates) const noexcept
Whether coordinates address a cell inside the grid.
Definition HexMap.h:122
std::int32_t cellCount() const noexcept
Total number of cells.
Definition HexMap.h:104
Reusable search scratch: one record per cell plus a priority bucket queue.
Definition HexSearch.h:68
std::vector< HexUnitState > snapshot() const
Serializable state of every unit, in id order.
Definition HexUnits.cpp:363
std::int32_t unitCount() const noexcept
Number of registered units.
Definition HexUnits.h:82
HexOccupancyQuery occupancyQuery() const
Occupancy predicate bound to this registry. @ownership The returned callable borrows *this; it must n...
Definition HexUnits.cpp:130
Result< HexUnitSample > advance(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, std::int32_t unitId, float dt)
Advances one unit's travel by dt seconds and reports where it now is.
Definition HexUnits.cpp:275
Result< void > removeUnit(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, std::int32_t unitId)
Removes a unit and withdraws its vision.
Definition HexUnits.cpp:165
bool isOccupied(HexCoordinates coordinates) const noexcept
Whether any unit occupies a cell.
Definition HexUnits.h:118
void removeAll(HexMap &map, HexVisibility &visibility) noexcept
Removes every unit and clears the visibility counters of the map.
Definition HexUnits.cpp:187
void refreshPositions(const HexMap &map) noexcept
Re-snaps every idle unit onto its cell's current position.
Definition HexUnits.cpp:345
void advanceAll(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, float dt)
Advances every travelling unit by dt seconds.
Definition HexUnits.cpp:320
void refreshVisibility(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch)
Re-derives the whole fog of war from the units' current cells.
Definition HexUnits.cpp:332
Result< void > beginTravel(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, std::int32_t unitId, const std::vector< std::int32_t > &path)
Starts travelling along path.
Definition HexUnits.cpp:219
std::int32_t unitIdAt(HexCoordinates coordinates) const noexcept
Id of the unit occupying a cell, or -1 when the cell is free.
Definition HexUnits.cpp:117
Result< void > restore(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, const std::vector< HexUnitState > &states)
Replaces every unit with states, granting their vision.
Definition HexUnits.cpp:375
Result< std::int32_t > addUnit(HexMap &map, HexVisibility &visibility, HexSearchContext &scratch, HexCoordinates location, float orientation)
Adds a unit on location and grants its vision.
Definition HexUnits.cpp:139
Fog-of-war bookkeeping: how many viewers currently see each cell.
Result< void > increase(HexMap &map, HexSearchContext &scratch, HexCoordinates from, std::int32_t range)
Adds one viewer at from.
void clear(HexMap &map) noexcept
Drops every viewer, leaving the map's explored flags untouched.
Result< void > decrease(HexMap &map, HexSearchContext &scratch, HexCoordinates from, std::int32_t range)
Removes one viewer at from.
eve::Diagnostic Diagnostic
std::function< bool(HexCoordinates)> HexOccupancyQuery
Predicate answering whether a cell is already occupied by another actor.
Definition HexSearch.h:143
bool isValidDestination(const HexMap &map, HexCoordinates coordinates, const HexOccupancyQuery &occupied)
Whether an actor may occupy a cell.
Axial coordinates of one hex cell.
constexpr std::int32_t distanceTo(const HexCoordinates &other) const noexcept
Cube distance to another cell (wrapping is not supported).
Snapshot of one unit used to drive rendering.
Definition HexUnits.h:40
Serializable state of one unit.
Definition HexUnits.h:32
float rotationSpeed
Turn rate in degrees per second while travelling.
Definition HexUnits.h:25
std::int32_t visionRange
Vision radius in cells, matching HexMoveRules::visionRange.
Definition HexUnits.h:21
float travelSpeed
Travel speed in path segments per second.
Definition HexUnits.h:23
Minimal 3-component float vector used by the hex mesh builders.
Definition HexMetrics.h:18