载入中...
搜索中...
未找到
HexSphereMap.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <string>
9#include <utility>
10
11namespace eve::hexmap {
12namespace {
13
15[[nodiscard]]
16[[nodiscard]] constexpr std::int32_t clampInt(std::int32_t value, std::int32_t low, std::int32_t high) noexcept {
17 return value < low ? low : (value > high ? high : value);
18}
19
21[[nodiscard]] constexpr std::int32_t layerIndex(std::int32_t feature) noexcept {
22 return feature == 0 ? 0 : (feature == 1 ? 1 : 2);
23}
24
25[[nodiscard]] float dot3(HexVec3 a, HexVec3 b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z; }
26
27[[nodiscard]] float length3(HexVec3 a) noexcept { return std::sqrt(dot3(a, a)); }
28
36[[nodiscard]] bool raySphereSpan(HexVec3 origin, HexVec3 direction, float radius, float& enter,
37 float& exit) noexcept {
38 const float b = dot3(origin, direction);
39 const float c = dot3(origin, origin) - radius * radius;
40 const float discriminant = b * b - c;
41 if (discriminant < 0.f) return false;
42 const float root = std::sqrt(discriminant);
43 enter = -b - root;
44 exit = -b + root;
45 return exit > 0.f;
46}
47
48} // namespace
49
50// --- construction -----------------------------------------------------------
51
52Result<void> HexSphereMap::reset(std::int32_t subdivision, float radius, std::uint32_t seed) {
53 if (subdivision < 0 || subdivision > kMaxHexSphereSubdivision)
55 Diagnostic::error(DiagnosticCode::InvalidArgument, "subdivision level is out of range", "hexmap.sphere"));
56 if (!std::isfinite(radius) || radius <= 0.f) return Result<void>::failure(
57 Diagnostic::error(DiagnosticCode::InvalidArgument, "sphere radius must be finite and positive", "hexmap.sphere"));
58
60 if (!built.ok()) return Result<void>::failure(built.status());
61
62 topology_ = std::move(built).takeValue();
63 sphereRadius_ = radius;
64 elevationStep_ = radius * kDefaultElevationRatio;
65 noise_.reset(seed);
66 cells_.assign(static_cast<std::size_t>(topology_.cellCount()), HexCellData{});
67 cellDirty_.assign(cells_.size(), 0u);
68 dirtyQueue_.clear();
69 dirtyHead_ = 0;
70 revision_ = 0;
72 return Result<void>::success();
73}
74
76 elevationStep_ = step;
77 ++revision_;
78}
79
80// --- geometry ---------------------------------------------------------------
81
83 if (!contains(cell)) return sphereRadius_;
84 return sphereRadius_ + static_cast<float>(elevation(cell)) * elevationStep_;
85}
86
87float HexSphereMap::cellSpacing() const noexcept {
88 const std::int32_t count = cellCount();
89 if (count < 2) return 0.f;
90 // Mean area per cell is `4 * pi / count` of the sphere, so the mean spacing of a
91 // hexagonal packing of that area is the square root of it.
92 return sphereRadius_ * std::sqrt(4.f * 3.14159265358979f / static_cast<float>(count));
93}
94
96 return topology_.direction(cell) * surfaceRadius(cell);
97}
98
100 return topology_.direction(cell) * sphereRadius_;
101}
102
104 if (!contains(a) || !contains(b)) return HexEdgeType::Flat;
105 if (topology_.directionOf(a, b) < 0) return HexEdgeType::Flat;
106 return edgeType(elevation(a), elevation(b));
107}
108
109// --- cell access ------------------------------------------------------------
110
112 if (!contains(cell)) return nullptr;
113 return &cells_[static_cast<std::size_t>(cell)];
114}
115
116HexCellData* HexSphereMap::mutableCell(HexSphereCell cell) noexcept {
117 if (!contains(cell)) return nullptr;
118 return &cells_[static_cast<std::size_t>(cell)];
119}
120
121std::int32_t HexSphereMap::elevation(HexSphereCell c) const noexcept {
122 const HexCellData* data = cellAt(c);
123 return data ? data->values.elevation() : 0;
124}
125std::int32_t HexSphereMap::waterLevel(HexSphereCell c) const noexcept {
126 const HexCellData* data = cellAt(c);
127 return data ? data->values.waterLevel() : 0;
128}
129std::int32_t HexSphereMap::terrainType(HexSphereCell c) const noexcept {
130 const HexCellData* data = cellAt(c);
131 return data ? data->values.terrainType() : 0;
132}
133std::int32_t HexSphereMap::urbanLevel(HexSphereCell c) const noexcept {
134 const HexCellData* data = cellAt(c);
135 return data ? data->values.urbanLevel() : 0;
136}
137std::int32_t HexSphereMap::farmLevel(HexSphereCell c) const noexcept {
138 const HexCellData* data = cellAt(c);
139 return data ? data->values.farmLevel() : 0;
140}
141std::int32_t HexSphereMap::plantLevel(HexSphereCell c) const noexcept {
142 const HexCellData* data = cellAt(c);
143 return data ? data->values.plantLevel() : 0;
144}
145std::int32_t HexSphereMap::specialIndex(HexSphereCell c) const noexcept {
146 const HexCellData* data = cellAt(c);
147 return data ? data->values.specialIndex() : 0;
148}
150 const HexCellData* data = cellAt(c);
151 return data && data->values.isUnderwater();
152}
154 const HexCellData* data = cellAt(c);
155 return data && data->flags.hasRiver();
156}
158 const HexCellData* data = cellAt(c);
159 return data && data->flags.hasRoad();
160}
161bool HexSphereMap::hasRiverThrough(HexSphereCell c, std::int32_t direction) const noexcept {
162 const HexCellData* data = cellAt(c);
163 if (!data || direction < 0 || direction >= kHexDirectionCount) return false;
164 return data->flags.hasRiverThrough(static_cast<HexDirection>(direction));
165}
167 const HexCellData* data = cellAt(c);
168 return data && data->flags.isWalled();
169}
171 const HexCellData* data = cellAt(c);
172 return data && data->flags.isExplored();
173}
175 const HexCellData* data = cellAt(c);
176 return data && data->flags.isExplorable();
177}
179 const HexCellData* data = cellAt(c);
180 return data ? data->values : HexValues{};
181}
183 const HexCellData* data = cellAt(c);
184 return data ? data->flags : HexFlags{};
185}
186
187// --- picking ----------------------------------------------------------------
188
190 if (empty())
192 Diagnostic::error(DiagnosticCode::NotFound, "hex sphere map is empty", "hexmap.sphere.pick"));
193 const float directionLength = length3(rayDirection);
194 if (!(directionLength > 1e-6f))
196 "ray direction must be non-zero",
197 "hexmap.sphere.pick"));
198 const HexVec3 direction = rayDirection * (1.f / directionLength);
199
200 // The surface is a step function of direction, so the ray is marched against
201 // the bounding sphere and then bisected. Intersecting a single sphere and
202 // refining would return the far-side cell whenever the camera sits between
203 // the surface and that bounding sphere, which is exactly the low-orbit case
204 // this map exists for.
205 const float boundingRadius =
206 sphereRadius_ + static_cast<float>(HexMetrics::kMaxElevation) * elevationStep_;
207 float enter = 0.f;
208 float exit = 0.f;
209 if (!raySphereSpan(rayOrigin, direction, boundingRadius, enter, exit))
211 Diagnostic::error(DiagnosticCode::NotFound, "ray misses the hex planet", "hexmap.sphere.pick"));
212
213 const float marchStep = std::max(boundingRadius * 0.01f, 1e-4f);
214 float tInside = 0.f;
216 for (float t = std::max(0.f, enter); t <= exit + marchStep; t += marchStep) {
217 const HexVec3 point = rayOrigin + direction * t;
218 const float span = length3(point);
219 if (span <= 1e-6f) continue;
220 const HexSphereCell cell = topology_.cellAt(point);
221 if (cell == kNoHexSphereCell) continue;
222 if (span <= surfaceRadius(cell)) {
223 hit = cell;
224 tInside = t;
225 break;
226 }
227 }
228 if (hit == kNoHexSphereCell)
230 DiagnosticCode::NotFound, "ray never reached the hex planet surface", "hexmap.sphere.pick"));
231
232 // Bisect between the last sample above the surface and the first one below it.
233 float tOutside = std::max(std::max(0.f, enter), tInside - marchStep);
234 for (std::int32_t iteration = 0; iteration < 24; ++iteration) {
235 const float middle = 0.5f * (tOutside + tInside);
236 const HexVec3 point = rayOrigin + direction * middle;
237 const float span = length3(point);
238 if (span <= 1e-6f) break;
239 const HexSphereCell cell = topology_.cellAt(point);
240 if (cell == kNoHexSphereCell) break;
241 if (span <= surfaceRadius(cell)) {
242 hit = cell;
243 tInside = middle;
244 } else {
245 tOutside = middle;
246 }
247 }
249}
250
251// --- neighbourhood queries --------------------------------------------------
252
254 if (!contains(a) || !contains(b)) return kNoHexSphereCell;
255 if (a == b) return 0;
256
257 std::vector<std::uint8_t> visited(cells_.size(), 0u);
258 std::vector<HexSphereCell> frontier;
259 std::vector<HexSphereCell> next;
260 frontier.push_back(a);
261 visited[static_cast<std::size_t>(a)] = 1u;
262
263 std::int32_t depth = 0;
264 while (!frontier.empty()) {
265 ++depth;
266 next.clear();
267 for (const HexSphereCell cell : frontier) {
268 const std::int32_t count = topology_.neighborCount(cell);
269 for (std::int32_t d = 0; d < count; ++d) {
270 const HexSphereCell neighbour = topology_.neighbor(cell, d);
271 if (neighbour == kNoHexSphereCell) continue;
272 if (visited[static_cast<std::size_t>(neighbour)]) continue;
273 if (neighbour == b) return depth;
274 visited[static_cast<std::size_t>(neighbour)] = 1u;
275 next.push_back(neighbour);
276 }
277 }
278 frontier.swap(next);
279 }
280 // The neighbour graph of a sphere is connected, so this is unreachable.
281 return kNoHexSphereCell;
282}
283
285 std::vector<HexSphereCell>& out) const {
286 out.clear();
287 if (!contains(center) || radius < 0) return;
288
289 std::vector<std::uint8_t> visited(cells_.size(), 0u);
290 out.push_back(center);
291 visited[static_cast<std::size_t>(center)] = 1u;
292
293 std::size_t head = 0;
294 for (std::int32_t step = 0; step < radius; ++step) {
295 const std::size_t levelEnd = out.size();
296 for (; head < levelEnd; ++head) {
297 const HexSphereCell cell = out[head];
298 const std::int32_t count = topology_.neighborCount(cell);
299 for (std::int32_t d = 0; d < count; ++d) {
300 const HexSphereCell neighbour = topology_.neighbor(cell, d);
301 if (neighbour == kNoHexSphereCell) continue;
302 if (visited[static_cast<std::size_t>(neighbour)]) continue;
303 visited[static_cast<std::size_t>(neighbour)] = 1u;
304 out.push_back(neighbour);
305 }
306 }
307 }
308}
309
310// --- dirty tracking ---------------------------------------------------------
311
312void HexSphereMap::markCellDirty(HexSphereCell cell) noexcept {
313 if (!contains(cell)) return;
314 if (cellDirty_[static_cast<std::size_t>(cell)]) return;
315 cellDirty_[static_cast<std::size_t>(cell)] = 1u;
316 dirtyQueue_.push_back(cell);
317}
318
320 if (!contains(cell)) return;
321 markCellDirty(cell);
322 const std::int32_t count = topology_.neighborCount(cell);
323 for (std::int32_t d = 0; d < count; ++d) markCellDirty(topology_.neighbor(cell, d));
324}
325
327 dirtyQueue_.clear();
328 dirtyHead_ = 0;
329 cellDirty_.assign(cells_.size(), 0u);
330 dirtyQueue_.reserve(cells_.size());
331 for (std::size_t i = 0; i < cells_.size(); ++i) {
332 cellDirty_[i] = 1u;
333 dirtyQueue_.push_back(static_cast<HexSphereCell>(i));
334 }
335}
336
337std::int32_t HexSphereMap::takeDirtyCell() noexcept {
338 while (dirtyHead_ < dirtyQueue_.size()) {
339 const HexSphereCell cell = dirtyQueue_[dirtyHead_++];
340 if (!contains(cell)) continue;
341 if (!cellDirty_[static_cast<std::size_t>(cell)]) continue;
342 cellDirty_[static_cast<std::size_t>(cell)] = 0u;
343 if (dirtyHead_ >= 4096u && dirtyHead_ * 2u >= dirtyQueue_.size()) {
344 dirtyQueue_.erase(dirtyQueue_.begin(),
345 dirtyQueue_.begin() + static_cast<std::ptrdiff_t>(dirtyHead_));
346 dirtyHead_ = 0u;
347 }
348 return cell;
349 }
350 dirtyQueue_.clear();
351 dirtyHead_ = 0u;
352 return kNoHexSphereCell;
353}
354
355// --- cell authoring ---------------------------------------------------------
356
357void HexSphereMap::refreshCellDependents(HexSphereCell cell) noexcept {
358 ++revision_;
359 markCellDirtyAndNeighbors(cell);
360}
361
362void HexSphereMap::validateRivers(HexSphereCell cell) noexcept {
363 const HexCellData* data = cellAt(cell);
364 if (!data || !data->flags.hasRiver()) return;
365 // A river may only leave a cell that is at least as high as its neighbour,
366 // unless the cell itself is a lake at exactly its own elevation.
367 const std::int32_t fromElevation = elevation(cell);
368 const std::int32_t fromWater = waterLevel(cell);
369 const std::int32_t count = neighborCount(cell);
370 for (std::int32_t d = 0; d < count; ++d) {
371 if (!data->flags.hasRiverOut(static_cast<HexDirection>(d))) continue;
372 const HexSphereCell neighbour = topology_.neighbor(cell, d);
373 if (neighbour == kNoHexSphereCell) continue;
374 const bool canFlow = fromElevation >= elevation(neighbour) || fromWater == fromElevation;
375 if (!canFlow) {
376 removeRiver(cell).ignore("river validation must keep the map consistent");
377 return;
378 }
379 }
380}
381
383 HexCellData* data = mutableCell(c);
384 if (!data) return Result<void>::failure(
385 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
386 const std::int32_t clamped = clampInt(value, HexMetrics::kMinElevation, HexMetrics::kMaxElevation);
387 if (data->values.elevation() == clamped) return Result<void>::success();
388 data->values = data->values.withElevation(clamped);
389
390 // Roads cannot span a difference of more than one elevation step.
391 bool hasInvalidRoad = false;
392 const std::int32_t count = neighborCount(c);
393 for (std::int32_t d = 0; d < count && !hasInvalidRoad; ++d) {
394 if (!data->flags.hasRoad(static_cast<HexDirection>(d))) continue;
395 const HexSphereCell neighbour = topology_.neighbor(c, d);
396 if (neighbour == kNoHexSphereCell) continue;
397 const std::int32_t delta = elevation(neighbour) - clamped;
398 if (delta > 1 || delta < -1) hasInvalidRoad = true;
399 }
400 if (hasInvalidRoad) removeRoads(c).ignore("elevation change must drop invalid roads");
401 refreshCellDependents(c);
402 validateRivers(c);
403 for (std::int32_t d = 0; d < count; ++d) {
404 const HexSphereCell neighbour = topology_.neighbor(c, d);
405 if (neighbour != kNoHexSphereCell) validateRivers(neighbour);
406 }
407 return Result<void>::success();
408}
409
411 HexCellData* data = mutableCell(c);
412 if (!data) return Result<void>::failure(
413 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
414 const std::int32_t clamped = clampInt(value, 0, HexMetrics::kMaxElevation);
415 if (data->values.waterLevel() == clamped) return Result<void>::success();
416 data->values = data->values.withWaterLevel(clamped);
417 refreshCellDependents(c);
418 validateRivers(c);
419 return Result<void>::success();
420}
421
423 HexCellData* data = mutableCell(c);
424 if (!data) return Result<void>::failure(
425 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
426 data->values = data->values.withTerrainType(clampTerrainType(value));
427 ++revision_;
428 return Result<void>::success();
429}
430
432 HexCellData* data = mutableCell(c);
433 if (!data) return Result<void>::failure(
434 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
435 data->values = data->values.withUrbanLevel(clampInt(value, 0, 3));
436 refreshCellDependents(c);
437 return Result<void>::success();
438}
439
441 HexCellData* data = mutableCell(c);
442 if (!data) return Result<void>::failure(
443 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
444 data->values = data->values.withFarmLevel(clampInt(value, 0, 3));
445 refreshCellDependents(c);
446 return Result<void>::success();
447}
448
450 HexCellData* data = mutableCell(c);
451 if (!data) return Result<void>::failure(
452 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
453 data->values = data->values.withPlantLevel(clampInt(value, 0, 3));
454 refreshCellDependents(c);
455 return Result<void>::success();
456}
457
459 HexCellData* data = mutableCell(c);
460 if (!data) return Result<void>::failure(
461 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
462 if (data->flags.hasRiver()) return Result<void>::success();
463 data->values = data->values.withSpecialIndex(clampInt(index, 0, 3));
464 removeRoads(c).ignore("special features replace roads");
465 refreshCellDependents(c);
466 return Result<void>::success();
467}
468
470 HexCellData* data = mutableCell(c);
471 if (!data) return Result<void>::failure(
472 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
473 data->flags = data->flags.withWalled(walled);
474 refreshCellDependents(c);
475 return Result<void>::success();
476}
477
479 HexCellData* data = mutableCell(c);
480 if (!data) return Result<void>::failure(
481 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
482 data->flags = data->flags.withExplored(explored);
483 // Fog state changes do not alter geometry, so the cells stay clean: only the
484 // revision is bumped so a renderer can tell that the visible set changed.
485 ++revision_;
486 return Result<void>::success();
487}
488
490 HexCellData* data = mutableCell(c);
491 if (!data) return Result<void>::failure(
492 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
493 data->flags = data->flags.withExplorable(explorable);
494 ++revision_;
495 return Result<void>::success();
496}
497
499 HexCellData* data = mutableCell(c);
500 if (!data) return Result<void>::failure(
501 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
502 data->values = values;
503 data->flags = flags;
504 refreshCellDependents(c);
505 return Result<void>::success();
506}
507
509 HexCellData* data = mutableCell(c);
510 if (!data) return Result<void>::failure(
511 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
512 if (direction < 0 || direction >= neighborCount(c))
514 Diagnostic::error(DiagnosticCode::InvalidArgument, "river direction is not an edge of the cell", "hexmap.sphere"));
515 const HexSphereCell neighbour = topology_.neighbor(c, direction);
516 if (neighbour == kNoHexSphereCell) return Result<void>::failure(
517 Diagnostic::error(DiagnosticCode::InvalidArgument, "river direction has no neighbour", "hexmap.sphere"));
518 const std::int32_t back = topology_.oppositeDirection(c, direction);
519 if (back < 0) return Result<void>::failure(
520 Diagnostic::error(DiagnosticCode::InvalidArgument, "river direction has no opposite edge", "hexmap.sphere"));
521
522 const auto out = static_cast<HexDirection>(direction);
523 const auto in = static_cast<HexDirection>(back);
524 if (data->flags.hasAnyRiverOut() && data->flags.hasRiverOut(out)) return Result<void>::success();
525 if (elevation(c) < elevation(neighbour) && waterLevel(c) != elevation(c))
527 Diagnostic::error(DiagnosticCode::InvalidArgument, "a river cannot flow uphill", "hexmap.sphere"));
528
529 // Drop only the links this call replaces, mirroring the reference cell's
530 // `RemoveOutgoingRiver` + same-edge `RemoveIncomingRiver`. Calling the full
531 // `removeRiver` here also erased the *incoming* link from the upstream cell,
532 // so carving a channel step by step wiped the step behind it and only the
533 // last edge survived.
534 const std::int32_t count = neighborCount(c);
535 for (std::int32_t d = 0; d < count; ++d) {
536 const auto previous = static_cast<HexDirection>(d);
537 if (!data->flags.hasRiverOut(previous)) continue;
538 data->flags = data->flags.withoutRiverOut(previous);
539 const HexSphereCell previousCell = topology_.neighbor(c, d);
540 const std::int32_t previousBack = topology_.oppositeDirection(c, d);
541 if (previousCell != kNoHexSphereCell && previousBack >= 0) {
542 HexCellData* previousData = mutableCell(previousCell);
543 if (previousData) previousData->flags = previousData->flags.withoutRiverIn(static_cast<HexDirection>(previousBack));
544 refreshCellDependents(previousCell);
545 }
546 }
547
548 data = mutableCell(c);
549 // A river may not leave and enter through the same edge.
550 if (data->flags.hasRiverIn(out)) {
551 data->flags = data->flags.withoutRiverIn(out);
552 HexCellData* other = mutableCell(neighbour);
553 if (other) other->flags = other->flags.withoutRiverOut(in);
554 }
555
556 data = mutableCell(c);
557 data->flags = data->flags.withRiverOut(out).withoutRoad(out);
558 data->values = data->values.withSpecialIndex(0);
559 HexCellData* other = mutableCell(neighbour);
560 other->flags = other->flags.withRiverIn(in).withoutRoad(in);
561 other->values = other->values.withSpecialIndex(0);
562 refreshCellDependents(c);
563 refreshCellDependents(neighbour);
564 return Result<void>::success();
565}
566
568 HexCellData* data = mutableCell(c);
569 if (!data) return Result<void>::failure(
570 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
571 if (!data->flags.hasRiver()) return Result<void>::success();
572 for (std::int32_t d = 0; d < kHexDirectionCount; ++d) {
573 const auto direction = static_cast<HexDirection>(d);
574 data->flags = data->flags.withoutRiverIn(direction).withoutRiverOut(direction);
575 }
576 const std::int32_t count = neighborCount(c);
577 for (std::int32_t d = 0; d < count; ++d) {
578 const HexSphereCell neighbour = topology_.neighbor(c, d);
579 const std::int32_t back = topology_.oppositeDirection(c, d);
580 if (neighbour == kNoHexSphereCell || back < 0) continue;
581 const auto in = static_cast<HexDirection>(back);
582 HexCellData* other = mutableCell(neighbour);
583 if (other) other->flags = other->flags.withoutRiverIn(in).withoutRiverOut(in);
584 refreshCellDependents(neighbour);
585 }
586 refreshCellDependents(c);
587 return Result<void>::success();
588}
589
591 HexCellData* data = mutableCell(c);
592 if (!data) return Result<void>::failure(
593 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
594 if (direction < 0 || direction >= neighborCount(c))
596 Diagnostic::error(DiagnosticCode::InvalidArgument, "road direction is not an edge of the cell", "hexmap.sphere"));
597 const HexSphereCell neighbour = topology_.neighbor(c, direction);
598 if (neighbour == kNoHexSphereCell) return Result<void>::failure(
599 Diagnostic::error(DiagnosticCode::InvalidArgument, "road direction has no neighbour", "hexmap.sphere"));
600 const std::int32_t back = topology_.oppositeDirection(c, direction);
601 if (back < 0) return Result<void>::failure(
602 Diagnostic::error(DiagnosticCode::InvalidArgument, "road direction has no opposite edge", "hexmap.sphere"));
603
604 const auto out = static_cast<HexDirection>(direction);
605 const auto in = static_cast<HexDirection>(back);
606 if (data->flags.hasRoad(out)) return Result<void>::success();
607 if (data->flags.hasRiverThrough(out)) return Result<void>::success();
608 if (data->flags.hasRiver() && !data->flags.hasRiverBeginOrEnd()) {
609 // A road may only join a river at a begin or end cell.
610 return Result<void>::success();
611 }
612 HexCellData* other = mutableCell(neighbour);
613 if (other->values.specialIndex() != 0 || data->values.specialIndex() != 0) return Result<void>::success();
614 if (other->flags.hasRiverThrough(in)) return Result<void>::success();
615 const std::int32_t delta = elevation(neighbour) - elevation(c);
616 if (delta > 1 || delta < -1) return Result<void>::success();
617
618 data->flags = data->flags.withRoad(out);
619 other->flags = other->flags.withRoad(in);
620 refreshCellDependents(c);
621 refreshCellDependents(neighbour);
622 return Result<void>::success();
623}
624
626 HexCellData* data = mutableCell(c);
627 if (!data) return Result<void>::failure(
628 Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex sphere map", "hexmap.sphere"));
629 if (!data->flags.hasRoad()) return Result<void>::success();
630 data->flags = data->flags.without(HexFlags::roadMask());
631 const std::int32_t count = neighborCount(c);
632 for (std::int32_t d = 0; d < count; ++d) {
633 const HexSphereCell neighbour = topology_.neighbor(c, d);
634 const std::int32_t back = topology_.oppositeDirection(c, d);
635 if (neighbour == kNoHexSphereCell || back < 0) continue;
636 HexCellData* other = mutableCell(neighbour);
637 if (other) other->flags = other->flags.withoutRoad(static_cast<HexDirection>(back));
638 refreshCellDependents(neighbour);
639 }
640 refreshCellDependents(c);
641 return Result<void>::success();
642}
643
644// --- brush authoring --------------------------------------------------------
645
648 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex sphere map", "hexmap.sphere"));
649 if (radius < 0) return Result<void>::failure(
650 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap.sphere"));
651 std::vector<HexSphereCell> cells;
653 for (const HexSphereCell cell : cells) {
654 const std::int32_t next = elevation(cell) + delta;
655 setElevation(cell, next).ignore("brush elevation edit clamps per cell");
656 }
657 return Result<void>::success();
658}
659
662 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex sphere map", "hexmap.sphere"));
663 if (radius < 0) return Result<void>::failure(
664 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap.sphere"));
665 std::vector<HexSphereCell> cells;
667 for (const HexSphereCell cell : cells)
668 setWaterLevel(cell, waterLevel(cell) + delta).ignore("brush water edit clamps per cell");
669 return Result<void>::success();
670}
671
672Result<void> HexSphereMap::editTerrainType(HexSphereCell center, std::int32_t radius, std::int32_t terrainType) {
674 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex sphere map", "hexmap.sphere"));
675 if (radius < 0) return Result<void>::failure(
676 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap.sphere"));
677 std::vector<HexSphereCell> cells;
679 for (const HexSphereCell cell : cells)
680 setTerrainType(cell, terrainType).ignore("brush terrain edit clamps per cell");
681 return Result<void>::success();
682}
683
685 std::int32_t delta) {
687 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex sphere map", "hexmap.sphere"));
688 if (radius < 0) return Result<void>::failure(
689 Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap.sphere"));
690 std::vector<HexSphereCell> cells;
692 const std::int32_t layer = layerIndex(feature);
693 for (const HexSphereCell cell : cells) {
694 const HexCellData* data = cellAt(cell);
695 if (!data || data->flags.hasRiver()) continue;
696 if (data->values.specialIndex() != 0) continue;
697 const std::int32_t current = layer == 0 ? data->values.urbanLevel()
698 : layer == 1 ? data->values.farmLevel()
699 : data->values.plantLevel();
700 const std::int32_t next = clampInt(current + delta, 0, 3);
701 if (layer == 0)
702 setUrbanLevel(cell, next).ignore("brush urban edit clamps per cell");
703 else if (layer == 1)
704 setFarmLevel(cell, next).ignore("brush farm edit clamps per cell");
705 else
706 setPlantLevel(cell, next).ignore("brush plant edit clamps per cell");
707 }
708 return Result<void>::success();
709}
710
711} // namespace eve::hexmap
double value
int root
Definition AnimSmr.cpp:119
Stable, structured diagnostics shared by engine modules.
std::map< std::string, Var > values
float u
Definition Grass.cpp:233
std::int32_t waterLevel
Editable spherical hex map over an icosahedral hex topology.
std::int32_t c
float elevation
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
graphics::Canvas * previous
TileLayer * layer
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::uint32_t seed
Definition PointSet.cpp:807
bool hit
float d
float t
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
double current
std::uint32_t count
Cell cell
float step
Definition TreeMesh.cpp:314
uint32_t index
std::uint32_t depth
glm::vec3 point
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
Packed boolean cell state (roads, rivers, walls, exploration).
Definition HexCell.h:123
constexpr HexFlags withoutRoad(HexDirection d) const noexcept
Without road.
Definition HexCell.h:200
constexpr bool hasRiverThrough(HexDirection d) const noexcept
Whether a river enters and/or leaves through d.
Definition HexCell.h:172
constexpr HexFlags withoutRiverIn(HexDirection d) const noexcept
Without river in.
Definition HexCell.h:207
constexpr HexFlags withRoad(HexDirection d) const noexcept
With road.
Definition HexCell.h:198
constexpr HexFlags withoutRiverOut(HexDirection d) const noexcept
Without river out.
Definition HexCell.h:217
constexpr HexFlags withRiverIn(HexDirection d) const noexcept
With river in.
Definition HexCell.h:202
static constexpr HexFlags roadMask() noexcept
Mask of every road bit.
Definition HexCell.h:237
static constexpr int kMinElevation
Minimum editable elevation.
Definition HexMetrics.h:160
static constexpr int kMaxElevation
Maximum editable elevation.
Definition HexMetrics.h:162
void reset(std::uint32_t seed) noexcept
Reseeds the field.
Definition HexNoise.h:35
std::int32_t urbanLevel(HexSphereCell c) const noexcept
std::int32_t waterLevel(HexSphereCell c) const noexcept
float cellSpacing() const noexcept
Mean centre-to-centre distance between neighbouring cells, in world units.
HexVec3 cellGroundPosition(HexSphereCell cell) const noexcept
World-space centre of the cell at radius, ignoring elevation.
void setElevationStep(float step) noexcept
Sets the radial world distance of one elevation level.
std::int32_t subdivision() const noexcept
The subdivision level this map was built at.
bool hasRiver(HexSphereCell c) const noexcept
Result< void > editTerrainType(HexSphereCell center, std::int32_t radius, std::int32_t terrainType)
Paints a terrain palette index over a hex brush.
Result< void > addRoad(HexSphereCell c, std::int32_t direction)
Adds a road through direction when both cells can carry one.
Result< void > editWaterLevel(HexSphereCell center, std::int32_t radius, std::int32_t delta)
Applies additive water-level change over a hex brush.
bool hasRiverThrough(HexSphereCell c, std::int32_t direction) const noexcept
std::uint32_t seed() const noexcept
The seed this map was built with.
Result< void > setExplorable(HexSphereCell c, bool explorable)
Sets whether a cell may ever be explored.
Result< void > removeRiver(HexSphereCell c)
Removes every river connection of the cell and its neighbours.
Result< void > setWaterLevel(HexSphereCell c, std::int32_t waterLevel)
Sets a cell's water level, clamped to [0, kMaxElevation].
bool isExplored(HexSphereCell c) const noexcept
Whether a cell has ever been seen by a viewer (the fog-of-war latch).
bool hasRoad(HexSphereCell c) const noexcept
std::int32_t takeDirtyCell() noexcept
Pops the next dirty cell.
bool isWalled(HexSphereCell c) const noexcept
bool isExplorable(HexSphereCell c) const noexcept
Whether a cell may ever be revealed by a viewer.
std::int32_t elevation(HexSphereCell c) const noexcept
Result< void > setFarmLevel(HexSphereCell c, std::int32_t level)
Sets a cell's farm level, clamped to [0, 3].
void collectBrush(HexSphereCell center, std::int32_t radius, std::vector< HexSphereCell > &out) const
Enumerates the cells within radius steps of center into out.
std::int32_t farmLevel(HexSphereCell c) const noexcept
Result< void > setUrbanLevel(HexSphereCell c, std::int32_t level)
Sets a cell's urban level, clamped to [0, 3].
void markCellDirtyAndNeighbors(HexSphereCell cell) noexcept
Marks one cell and its direct neighbours dirty.
Result< void > editElevation(HexSphereCell center, std::int32_t radius, std::int32_t delta)
Raises or lowers every cell within radius steps of center.
Result< void > setExplored(HexSphereCell c, bool explored)
Sets the explored (fog-of-war) latch of a cell.
void markAllDirty() noexcept
Marks every cell dirty.
bool empty() const noexcept
Whether the map holds any cell.
Result< void > setElevation(HexSphereCell c, std::int32_t elevation)
Sets a cell's elevation, clamped to the editable range, and refreshes dependents.
HexFlags flags(HexSphereCell c) const noexcept
Full packed flag record of a cell (zero when out of range).
Result< void > removeRoads(HexSphereCell c)
Removes every road of the cell and its neighbours.
Result< void > setOutgoingRiver(HexSphereCell c, std::int32_t direction)
Forces a river to leave the cell through direction, mirroring the neighbour.
std::int32_t distance(HexSphereCell a, HexSphereCell b) const
Number of edges between two cells, or kNoHexSphereCell when either is invalid.
HexValues values(HexSphereCell c) const noexcept
Full packed value record of a cell (zero when out of range).
std::int32_t plantLevel(HexSphereCell c) const noexcept
Result< void > setTerrainType(HexSphereCell c, std::int32_t terrainType)
Sets a cell's terrain palette index, clamped to the palette range.
const HexCellData * cellAt(HexSphereCell cell) const noexcept
Cell record by id, or null when out of range. @ownership Borrowed; the map owns the record....
float surfaceRadius(HexSphereCell cell) const noexcept
Radius of the cell's surface: sphereRadius + elevation * elevationStep.
HexEdgeType edgeTypeTo(HexSphereCell a, HexSphereCell b) const noexcept
Relationship between two adjacent cells; Flat when not adjacent.
Result< void > setWalled(HexSphereCell c, bool walled)
Sets whether a cell is walled.
std::int32_t cellCount() const noexcept
Number of cells (10 * f^2 + 2).
std::int32_t terrainType(HexSphereCell c) const noexcept
std::int32_t neighborCount(HexSphereCell cell) const noexcept
Number of edges of cell (5 or 6), or 0 when out of range.
Result< void > reset(std::int32_t subdivision, float radius, std::uint32_t seed)
Replaces the map with a new sphere of 10 * 4^subdivision + 2 cells.
Result< void > setCellState(HexSphereCell c, HexValues values, HexFlags flags)
Writes a complete cell record verbatim.
HexVec3 cellPosition(HexSphereCell cell) const noexcept
World-space centre of the cell's surface.
Result< void > setPlantLevel(HexSphereCell c, std::int32_t level)
Sets a cell's plant level, clamped to [0, 3].
Result< HexSphereCell > pickCell(HexVec3 rayOrigin, HexVec3 rayDirection) const
Finds the cell under a world-space ray.
static constexpr float kDefaultElevationRatio
Ratio of the default radial elevation step to the sphere radius.
Result< void > setSpecialIndex(HexSphereCell c, std::int32_t index)
Sets a cell's special-feature index; ignored while the cell carries a river.
std::int32_t specialIndex(HexSphereCell c) const noexcept
Result< void > editFeatureLevel(HexSphereCell center, std::int32_t radius, std::int32_t feature, std::int32_t delta)
Applies additive change to one feature level (0 urban, 1 farm, 2 plant).
bool isUnderwater(HexSphereCell c) const noexcept
bool contains(HexSphereCell cell) const noexcept
Whether cell is a valid id of this map.
std::int32_t neighborCount(HexSphereCell cell) const noexcept
Number of edges of cell (5 or 6), or 0 when out of range.
std::int32_t oppositeDirection(HexSphereCell cell, std::int32_t direction) const noexcept
Edge index of cell's edge towards its neighbour across direction.
std::int32_t cellCount() const noexcept
Number of cells (10 * f^2 + 2).
HexSphereCell neighbor(HexSphereCell cell, std::int32_t direction) const noexcept
Neighbour of cell across edge direction.
static Result< HexSphereTopology > build(std::int32_t subdivision)
Builds the topology of one subdivision level.
HexSphereCell cellAt(HexVec3 unitDirection) const noexcept
The cell whose centre direction is nearest to unitDirection.
Packed numeric cell state.
Definition HexCell.h:38
constexpr HexValues withSpecialIndex(std::int32_t value) const noexcept
With special index.
Definition HexCell.h:96
constexpr std::int32_t specialIndex() const noexcept
Special index.
Definition HexCell.h:61
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
Definition HexMetrics.h:62
constexpr HexEdgeType edgeType(int elevation1, int elevation2) noexcept
The relationship between two elevations (single-step changes are slopes).
Definition HexMetrics.h:96
constexpr std::int32_t kMaxHexSphereSubdivision
Largest supported subdivision level; 10 * 4^7 + 2 = 163842 cells.
HexEdgeType
Relationship between two neighbouring cells of different elevation.
Definition HexMetrics.h:93
constexpr HexSphereCell kNoHexSphereCell
Returned by a spherical cell query that has no answer.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
std::int32_t HexSphereCell
Dense identifier of one cell of a spherical hex topology.
constexpr std::int32_t clampTerrainType(std::int32_t index) noexcept
Terrain type index clamped into the supported palette range.
Definition HexCell.h:24
HexDirection
Hex facing directions, counter-clockwise from north-east.
Definition HexMetrics.h:59
One cell of the hex map: packed values plus packed flags.
Definition HexCell.h:244
Minimal 3-component float vector used by the hex mesh builders.
Definition HexMetrics.h:18