载入中...
搜索中...
未找到
HexMap.cpp
浏览该文件的文档.
1#include "hexmap/HexMap.h"
2
3#include "common/Diagnostic.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8
9namespace eve::hexmap {
10namespace {
11
12[[nodiscard]]
13[[nodiscard]] std::int32_t clampInt(std::int32_t value, std::int32_t low, std::int32_t high) noexcept {
14 return value < low ? low : (value > high ? high : value);
15}
16
17[[nodiscard]] std::int32_t layerIndex(std::int32_t feature) noexcept { return clampInt(feature, 0, 2); }
18
26[[nodiscard]] std::int32_t saturatingAdd(std::int32_t value, std::int32_t delta) noexcept {
27 const std::int64_t sum = static_cast<std::int64_t>(value) + static_cast<std::int64_t>(delta);
28 if (sum < std::numeric_limits<std::int32_t>::min()) return std::numeric_limits<std::int32_t>::min();
29 if (sum > std::numeric_limits<std::int32_t>::max()) return std::numeric_limits<std::int32_t>::max();
30 return static_cast<std::int32_t>(sum);
31}
32
33} // namespace
34
35// --- construction -----------------------------------------------------------
36
37Result<void> HexMap::reset(std::int32_t cellCountX, std::int32_t cellCountZ, std::uint32_t seed) {
38 if (cellCountX <= 0 || cellCountZ <= 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "hex map size must be positive", "hexmap"));
40 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "hex map size must be a multiple of the 5x5 chunk size", "hexmap"));
41
42 cellCountX_ = cellCountX;
43 cellCountZ_ = cellCountZ;
44 chunkCountX_ = cellCountX / HexMetrics::kChunkSizeX;
45 chunkCountZ_ = cellCountZ / HexMetrics::kChunkSizeZ;
46 // `HexValues` stores the elevation biased by +15, so a zeroed record would read
47 // back as -15. Every cell therefore starts explicitly at elevation 0 and water 0.
48 // Every cell also starts *explorable but unexplored*, which is the fog-of-war
49 // precondition of the reference project: a viewer may reveal it, nothing has yet.
50 HexCellData initial{};
51 initial.values = initial.values.withElevation(0).withWaterLevel(0);
52 initial.flags = initial.flags.withExplorable(true).withExplored(false);
53 cells_.assign(static_cast<std::size_t>(cellCountX_) * static_cast<std::size_t>(cellCountZ_), initial);
54 chunkDirty_.assign(static_cast<std::size_t>(chunkCount()), 0u);
55 dirtyQueue_.clear();
56 noise_.reset(seed);
57 revision_ = 0;
59 return Result<void>::success();
60}
61
62// --- topology ---------------------------------------------------------------
63
64std::int32_t HexMap::indexOf(HexCoordinates coordinates) const noexcept {
65 if (!contains(coordinates)) return -1;
66 return coordinates.offsetX() + coordinates.z * cellCountX_;
67}
68
69HexCoordinates HexMap::coordinatesAt(std::int32_t index) const noexcept {
70 if (index < 0 || index >= cellCount()) return HexCoordinates{};
71 return HexCoordinates::fromOffset(index % cellCountX_, index / cellCountX_);
72}
73
74const HexCellData* HexMap::cellAt(std::int32_t index) const noexcept {
75 if (index < 0 || index >= cellCount()) return nullptr;
76 return &cells_[static_cast<std::size_t>(index)];
77}
78
79const HexCellData* HexMap::cell(HexCoordinates coordinates) const noexcept { return cellAt(indexOf(coordinates)); }
80
81HexCellData* HexMap::mutableCell(HexCoordinates coordinates) noexcept {
82 const std::int32_t index = indexOf(coordinates);
83 if (index < 0) return nullptr;
84 return &cells_[static_cast<std::size_t>(index)];
85}
86
88 const HexCoordinates candidate = coordinates.step(direction);
89 if (!contains(candidate)) return false;
90 out = candidate;
91 return true;
92}
93
95 if (!contains(a) || !contains(b)) return HexEdgeType::Flat;
96 if (a.distanceTo(b) != 1) return HexEdgeType::Flat;
97 return edgeType(elevation(a), elevation(b));
98}
99
100// --- chunking ---------------------------------------------------------------
101
102std::int32_t HexMap::chunkIndexOf(HexCoordinates coordinates) const noexcept {
103 if (!contains(coordinates)) return -1;
104 return coordinates.chunkColumn() + coordinates.chunkRow() * chunkCountX_;
105}
106
107std::int32_t HexMap::chunkColumnOf(std::int32_t chunkIndex) const noexcept {
108 return chunkCountX_ > 0 ? chunkIndex % chunkCountX_ : 0;
109}
110
111std::int32_t HexMap::chunkRowOf(std::int32_t chunkIndex) const noexcept {
112 return chunkCountX_ > 0 ? chunkIndex / chunkCountX_ : 0;
113}
114
115HexCoordinates HexMap::chunkCell(std::int32_t chunkIndex, std::int32_t column, std::int32_t row) const noexcept {
116 const std::int32_t offsetX = chunkColumnOf(chunkIndex) * HexMetrics::kChunkSizeX + column;
117 const std::int32_t offsetZ = chunkRowOf(chunkIndex) * HexMetrics::kChunkSizeZ + row;
118 return HexCoordinates::fromOffset(offsetX, offsetZ);
119}
120
121HexVec3 HexMap::chunkCenter(std::int32_t chunkIndex) const noexcept {
122 if (chunkIndex < 0 || chunkIndex >= chunkCount()) return HexVec3{};
123 const HexVec3 a = cellPosition(chunkCell(chunkIndex, 0, 0));
124 const HexVec3 b = cellPosition(chunkCell(chunkIndex, HexMetrics::kChunkSizeX - 1, HexMetrics::kChunkSizeZ - 1));
125 return HexVec3{(a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f, (a.z + b.z) * 0.5f};
126}
127
128void HexMap::markChunkDirty(std::int32_t chunkIndex) noexcept {
129 if (chunkIndex < 0 || chunkIndex >= chunkCount()) return;
130 auto& flag = chunkDirty_[static_cast<std::size_t>(chunkIndex)];
131 if (flag != 0u) return;
132 flag = 1u;
133 dirtyQueue_.push_back(chunkIndex);
134}
135
136void HexMap::markChunkDirtyAndNeighbors(std::int32_t chunkIndex) noexcept {
137 if (chunkIndex < 0 || chunkIndex >= chunkCount()) return;
138 markChunkDirty(chunkIndex);
139 const std::int32_t column = chunkColumnOf(chunkIndex);
140 const std::int32_t row = chunkRowOf(chunkIndex);
141 for (std::int32_t dz = -1; dz <= 1; ++dz) {
142 for (std::int32_t dx = -1; dx <= 1; ++dx) {
143 if (dx == 0 && dz == 0) continue;
144 const std::int32_t cx = column + dx;
145 const std::int32_t cz = row + dz;
146 if (cx < 0 || cx >= chunkCountX_ || cz < 0 || cz >= chunkCountZ_) continue;
147 markChunkDirty(cx + cz * chunkCountX_);
148 }
149 }
150}
151
153 dirtyQueue_.clear();
154 for (std::int32_t i = 0; i < chunkCount(); ++i) {
155 chunkDirty_[static_cast<std::size_t>(i)] = 1u;
156 dirtyQueue_.push_back(i);
157 }
158}
159
160std::int32_t HexMap::takeDirtyChunk() noexcept {
161 if (dirtyQueue_.empty()) return -1;
162 const std::int32_t chunkIndex = dirtyQueue_.back();
163 dirtyQueue_.pop_back();
164 chunkDirty_[static_cast<std::size_t>(chunkIndex)] = 0u;
165 return chunkIndex;
166}
167
168// --- geometry ---------------------------------------------------------------
169
170HexVec3 HexMap::cellPosition(HexCoordinates coordinates) const noexcept {
172 const std::int32_t level = elevation(coordinates);
173 position.y = HexMetrics::elevationY(level) + noise_.elevationPerturb(position.x, position.z);
174 return position;
175}
176
177Result<HexCoordinates> HexMap::pickCell(HexVec3 rayOrigin, HexVec3 rayDirection) const noexcept {
178 if (empty())
180 Diagnostic::error(DiagnosticCode::NotFound, "hex map is empty", "hexmap.pick"));
181
182 const float dirLength =
183 std::sqrt(rayDirection.x * rayDirection.x + rayDirection.y * rayDirection.y + rayDirection.z * rayDirection.z);
184 if (dirLength <= 1e-6f)
186 Diagnostic::error(DiagnosticCode::InvalidArgument, "ray direction must be non-zero", "hexmap.pick"));
187 rayDirection.x /= dirLength;
188 rayDirection.y /= dirLength;
189 rayDirection.z /= dirLength;
190
191 // Bounding box of the whole map, padded by one cell and one elevation step.
192 HexVec3 low = cellGroundPosition(HexCoordinates::fromOffset(0, 0));
193 HexVec3 high = cellGroundPosition(HexCoordinates::fromOffset(cellCountX_ - 1, cellCountZ_ - 1));
194 const float pad = HexMetrics::kOuterRadius;
195 low.x -= pad;
196 low.z -= pad;
198 high.x += pad;
199 high.z += pad;
201
202 float tEnter = 0.f;
203 float tExit = std::numeric_limits<float>::max();
204 const float origin[3] = {rayOrigin.x, rayOrigin.y, rayOrigin.z};
205 const float direction[3] = {rayDirection.x, rayDirection.y, rayDirection.z};
206 const float boxLow[3] = {low.x, low.y, low.z};
207 const float boxHigh[3] = {high.x, high.y, high.z};
208 for (int axis = 0; axis < 3; ++axis) {
209 if (std::fabs(direction[axis]) < 1e-8f) {
210 if (origin[axis] < boxLow[axis] || origin[axis] > boxHigh[axis]) {
212 Diagnostic::error(DiagnosticCode::NotFound, "ray misses the hex map bounds", "hexmap.pick"));
213 }
214 continue;
215 }
216 const float inv = 1.f / direction[axis];
217 float t0 = (boxLow[axis] - origin[axis]) * inv;
218 float t1 = (boxHigh[axis] - origin[axis]) * inv;
219 if (t0 > t1) std::swap(t0, t1);
220 tEnter = std::max(tEnter, t0);
221 tExit = std::min(tExit, t1);
222 }
223 if (tExit < tEnter)
225 Diagnostic::error(DiagnosticCode::NotFound, "ray misses the hex map bounds", "hexmap.pick"));
226
227 // The march only has to cover the span in which the ray can still meet a surface:
228 // the box entry/exit clipped by the vertical band every cell lies in. A fixed step
229 // budget (this used to be 512 steps of 1.5 world units, i.e. 768 units past the box
230 // entry) silently reported NotFound for hits further along the ray - reachable on a
231 // large grid or with a long grazing ray. Deriving the count from the clipped span
232 // keeps the worst case bounded by the box itself, because a normalised direction
233 // always has at least one component large enough to leave the box in finite time.
234 float marchStart = tEnter;
235 float marchEnd = tExit;
236 if (std::fabs(direction[1]) >= 1e-8f) {
237 const float invY = 1.f / direction[1];
238 float t0 = (boxLow[1] - origin[1]) * invY;
239 float t1 = (boxHigh[1] - origin[1]) * invY;
240 if (t0 > t1) std::swap(t0, t1);
241 marchStart = std::max(marchStart, t0);
242 marchEnd = std::min(marchEnd, t1);
243 }
244 if (marchEnd < marchStart)
246 Diagnostic::error(DiagnosticCode::NotFound, "ray never reached the hex surface", "hexmap.pick"));
247
248 const float step = HexMetrics::kElevationStep * 0.5f;
249 const float maxRayLength = marchEnd;
250 const int maxSteps = static_cast<int>((marchEnd - marchStart) / step) + 2;
251 float previousT = marchStart;
252 HexCoordinates previousCoordinates{};
253 bool havePrevious = false;
254
255 for (int i = 0; i <= maxSteps; ++i) {
256 const float t = std::min(marchStart + static_cast<float>(i) * step, maxRayLength);
257 const HexVec3 point{rayOrigin.x + rayDirection.x * t, rayOrigin.y + rayDirection.y * t,
258 rayOrigin.z + rayDirection.z * t};
260 if (contains(coordinates)) {
261 const float surfaceY = cellPosition(coordinates).y;
262 if (point.y <= surfaceY) {
263 // Bisect between the last sample above the surface and this one.
264 float lo = havePrevious ? previousT : marchStart;
265 float hi = t;
266 // Seed with the sample that actually hit: the previous in-grid sample
267 // when the ray entered from above, otherwise this one. Seeding from
268 // `previousCoordinates` unconditionally would fall back to its default
269 // (0, 0), which `contains` accepts on every non-empty map, so a ray
270 // that first meets the surface already inside the grid returned (0, 0).
271 HexCoordinates best = havePrevious ? previousCoordinates : coordinates;
272 for (int iteration = 0; iteration < 8; ++iteration) {
273 const float mid = (lo + hi) * 0.5f;
274 const HexVec3 sample{rayOrigin.x + rayDirection.x * mid, rayOrigin.y + rayDirection.y * mid,
275 rayOrigin.z + rayDirection.z * mid};
276 const HexCoordinates candidate = HexCoordinates::fromWorldPosition(sample);
277 if (!contains(candidate)) break;
278 best = candidate;
279 if (sample.y <= cellPosition(candidate).y)
280 hi = mid;
281 else
282 lo = mid;
283 }
284 if (contains(best)) return Result<HexCoordinates>::success(best);
285 return Result<HexCoordinates>::success(coordinates);
286 }
287 previousCoordinates = coordinates;
288 havePrevious = true;
289 }
290 previousT = t;
291 if (t >= maxRayLength) break;
292 }
293
295 Diagnostic::error(DiagnosticCode::NotFound, "ray never reached the hex surface", "hexmap.pick"));
296}
297
298// --- cell queries -----------------------------------------------------------
299
300std::int32_t HexMap::elevation(HexCoordinates c) const noexcept {
301 const HexCellData* data = cell(c);
302 return data ? data->values.elevation() : 0;
303}
304std::int32_t HexMap::waterLevel(HexCoordinates c) const noexcept {
305 const HexCellData* data = cell(c);
306 return data ? data->values.waterLevel() : 0;
307}
308std::int32_t HexMap::terrainType(HexCoordinates c) const noexcept {
309 const HexCellData* data = cell(c);
310 return data ? data->values.terrainType() : 0;
311}
312std::int32_t HexMap::urbanLevel(HexCoordinates c) const noexcept {
313 const HexCellData* data = cell(c);
314 return data ? data->values.urbanLevel() : 0;
315}
316std::int32_t HexMap::farmLevel(HexCoordinates c) const noexcept {
317 const HexCellData* data = cell(c);
318 return data ? data->values.farmLevel() : 0;
319}
320std::int32_t HexMap::plantLevel(HexCoordinates c) const noexcept {
321 const HexCellData* data = cell(c);
322 return data ? data->values.plantLevel() : 0;
323}
324std::int32_t HexMap::specialIndex(HexCoordinates c) const noexcept {
325 const HexCellData* data = cell(c);
326 return data ? data->values.specialIndex() : 0;
327}
329 const HexCellData* data = cell(c);
330 return data ? data->values.isUnderwater() : false;
331}
332bool HexMap::hasRiver(HexCoordinates c) const noexcept {
333 const HexCellData* data = cell(c);
334 return data ? data->flags.hasRiver() : false;
335}
336bool HexMap::hasRoad(HexCoordinates c) const noexcept {
337 const HexCellData* data = cell(c);
338 return data ? data->flags.hasRoad() : false;
339}
341 const HexCellData* data = cell(c);
342 return data ? data->flags.hasRiverThrough(d) : false;
343}
344bool HexMap::isWalled(HexCoordinates c) const noexcept {
345 const HexCellData* data = cell(c);
346 return data ? data->flags.isWalled() : false;
347}
348bool HexMap::isExplored(HexCoordinates c) const noexcept {
349 const HexCellData* data = cell(c);
350 return data ? data->flags.isExplored() : false;
351}
353 const HexCellData* data = cell(c);
354 return data ? data->flags.isExplorable() : false;
355}
357 const HexCellData* data = cell(c);
358 return data ? data->values : HexValues{};
359}
361 const HexCellData* data = cell(c);
362 return data ? data->flags : HexFlags{};
363}
364
365// --- cell authoring ---------------------------------------------------------
366
367void HexMap::refreshCellDependents(HexCoordinates coordinates) noexcept {
368 ++revision_;
369 markChunkDirtyAndNeighbors(chunkIndexOf(coordinates));
370}
371
372void HexMap::validateRivers(HexCoordinates coordinates) noexcept {
373 const HexCellData* data = cell(coordinates);
374 if (!data || !data->flags.hasRiver()) return;
375 // A river may only leave a cell that is at least as high as its neighbour,
376 // unless the cell itself is a lake at exactly its own elevation.
377 const std::int32_t fromElevation = elevation(coordinates);
378 const std::int32_t fromWater = waterLevel(coordinates);
379 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
380 const HexDirection direction = static_cast<HexDirection>(i);
381 if (!data->flags.hasRiverOut(direction)) continue;
382 HexCoordinates neighbour{};
383 const bool canFlow = getNeighbor(coordinates, direction, neighbour) &&
384 (fromElevation >= elevation(neighbour) || fromWater == fromElevation);
385 if (!canFlow) {
386 removeRiver(coordinates).ignore("river validation must keep the map consistent");
387 return;
388 }
389 }
390}
391
393 HexCellData* data = mutableCell(c);
394 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
395 const std::int32_t clamped = clampInt(value, HexMetrics::kMinElevation, HexMetrics::kMaxElevation);
396 if (data->values.elevation() == clamped) return Result<void>::success();
397 data->values = data->values.withElevation(clamped);
398
399 // Roads cannot span a difference of more than one elevation step.
400 bool hasInvalidRoad = false;
401 for (std::int32_t i = 0; i < kHexDirectionCount && !hasInvalidRoad; ++i) {
402 const HexDirection direction = static_cast<HexDirection>(i);
403 if (!data->flags.hasRoad(direction)) continue;
404 HexCoordinates neighbour{};
405 if (!getNeighbor(c, direction, neighbour)) continue;
406 const std::int32_t delta = elevation(neighbour) - clamped;
407 if (delta > 1 || delta < -1) hasInvalidRoad = true;
408 }
409 if (hasInvalidRoad) removeRoads(c).ignore("elevation change must drop invalid roads");
410 refreshCellDependents(c);
411 validateRivers(c);
412 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
413 HexCoordinates neighbour{};
414 if (getNeighbor(c, static_cast<HexDirection>(i), neighbour)) validateRivers(neighbour);
415 }
416 return Result<void>::success();
417}
418
420 HexCellData* data = mutableCell(c);
421 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
422 const std::int32_t clamped = clampInt(value, 0, HexMetrics::kMaxElevation);
423 if (data->values.waterLevel() == clamped) return Result<void>::success();
424 data->values = data->values.withWaterLevel(clamped);
425 refreshCellDependents(c);
426 validateRivers(c);
427 return Result<void>::success();
428}
429
431 HexCellData* data = mutableCell(c);
432 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
433 data->values = data->values.withTerrainType(clampTerrainType(value));
434 // Terrain type is baked into the terrain mesh's vertex encoding, so the chunk
435 // must be rebuilt just like any other authoring write.
436 refreshCellDependents(c);
437 return Result<void>::success();
438}
439
441 HexCellData* data = mutableCell(c);
442 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
443 data->values = data->values.withUrbanLevel(clampInt(value, 0, 3));
444 refreshCellDependents(c);
445 return Result<void>::success();
446}
447
449 HexCellData* data = mutableCell(c);
450 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
451 data->values = data->values.withFarmLevel(clampInt(value, 0, 3));
452 refreshCellDependents(c);
453 return Result<void>::success();
454}
455
457 HexCellData* data = mutableCell(c);
458 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
459 data->values = data->values.withPlantLevel(clampInt(value, 0, 3));
460 refreshCellDependents(c);
461 return Result<void>::success();
462}
463
465 HexCellData* data = mutableCell(c);
466 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
467 if (data->flags.hasRiver()) return Result<void>::success();
468 data->values = data->values.withSpecialIndex(clampInt(index, 0, 3));
469 removeRoads(c).ignore("special features replace roads");
470 refreshCellDependents(c);
471 return Result<void>::success();
472}
473
475 HexCellData* data = mutableCell(c);
476 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
477 data->flags = data->flags.withWalled(walled);
478 refreshCellDependents(c);
479 return Result<void>::success();
480}
481
483 HexCellData* data = mutableCell(c);
484 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
485 data->flags = data->flags.withExplored(explored);
486 // The fog overlay *is* geometry derived from this latch: `buildFogMesh` emits a
487 // column shaded 1 while the cell is unexplored, 0 once it is explored but unseen,
488 // and nothing at all while it is visible. Flipping the latch therefore changes the
489 // chunk mesh, which is why `HexVisibility::increase` dirties the chunk itself.
490 refreshCellDependents(c);
491 return Result<void>::success();
492}
493
495 HexCellData* data = mutableCell(c);
496 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
497 data->flags = data->flags.withExplorable(explorable);
498 // Not rendered on its own, but it gates every later visibility sweep, so keep the
499 // documented "every mutation dirties its chunk" invariant unconditional rather
500 // than leave a fact that a future fog rule could read without a rebuild.
501 refreshCellDependents(c);
502 return Result<void>::success();
503}
504
506 HexCellData* data = mutableCell(c);
507 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
508 data->values = values;
509 data->flags = flags;
510 refreshCellDependents(c);
511 return Result<void>::success();
512}
513
515 HexCellData* data = mutableCell(c);
516 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
517 HexCoordinates neighbour{};
518 if (!getNeighbor(c, direction, neighbour)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "river direction leaves the hex map", "hexmap"));
519 if (data->flags.hasAnyRiverOut() && data->flags.hasRiverOut(direction)) return Result<void>::success();
520 if (elevation(c) < elevation(neighbour) && waterLevel(c) != elevation(c))
521 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "a river cannot flow uphill", "hexmap"));
522
523 // Drop only the links this call replaces, mirroring the reference cell's
524 // `RemoveOutgoingRiver` + same-edge `RemoveIncomingRiver`. Calling the full
525 // `removeRiver` here also erased the *incoming* link from the upstream cell, so
526 // carving a channel step by step wiped the step behind it and only the last
527 // edge survived; that is why a 40-cell walk produced a 2-cell river.
528 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
529 const HexDirection previous = static_cast<HexDirection>(i);
530 if (!data->flags.hasRiverOut(previous)) continue;
531 data->flags = data->flags.withoutRiverOut(previous);
532 HexCoordinates previousNeighbour{};
533 if (getNeighbor(c, previous, previousNeighbour)) {
534 HexCellData* previousData = mutableCell(previousNeighbour);
535 previousData->flags = previousData->flags.withoutRiverIn(opposite(previous));
536 refreshCellDependents(previousNeighbour);
537 }
538 }
539 data = mutableCell(c);
540 // A river may not leave and enter through the same edge.
541 if (data->flags.hasRiverIn(direction)) {
542 data->flags = data->flags.withoutRiverIn(direction);
543 HexCellData* otherData = mutableCell(neighbour);
544 otherData->flags = otherData->flags.withoutRiverOut(opposite(direction));
545 }
546
547 data = mutableCell(c);
548 data->flags = data->flags.withRiverOut(direction).withoutRoad(direction);
549 data->values = data->values.withSpecialIndex(0);
550 HexCellData* other = mutableCell(neighbour);
552 other->values = other->values.withSpecialIndex(0);
553 refreshCellDependents(c);
554 refreshCellDependents(neighbour);
555 return Result<void>::success();
556}
557
559 HexCellData* data = mutableCell(c);
560 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
561 if (!data->flags.hasRiver()) return Result<void>::success();
562 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
563 const HexDirection direction = static_cast<HexDirection>(i);
564 data->flags = data->flags.withoutRiverIn(direction).withoutRiverOut(direction);
565 }
566 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
567 const HexDirection direction = static_cast<HexDirection>(i);
568 HexCoordinates neighbour{};
569 if (!getNeighbor(c, direction, neighbour)) continue;
570 HexCellData* other = mutableCell(neighbour);
572 refreshCellDependents(neighbour);
573 }
574 refreshCellDependents(c);
575 return Result<void>::success();
576}
577
579 HexCellData* data = mutableCell(c);
580 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
581 HexCoordinates neighbour{};
582 if (!getNeighbor(c, direction, neighbour)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "road direction leaves the hex map", "hexmap"));
583 if (data->flags.hasRoad(direction)) return Result<void>::success();
584 if (data->flags.hasRiverThrough(direction)) return Result<void>::success();
585 if (data->flags.hasRiver() && !data->flags.hasRiverBeginOrEnd()) {
586 // A road may only join a river at a begin or end cell.
587 return Result<void>::success();
588 }
589 HexCellData* other = mutableCell(neighbour);
590 if (other->values.specialIndex() != 0 || data->values.specialIndex() != 0) return Result<void>::success();
592 const std::int32_t delta = elevation(neighbour) - elevation(c);
593 if (delta > 1 || delta < -1) return Result<void>::success();
594
595 data->flags = data->flags.withRoad(direction);
596 other->flags = other->flags.withRoad(opposite(direction));
597 refreshCellDependents(c);
598 refreshCellDependents(neighbour);
599 return Result<void>::success();
600}
601
603 HexCellData* data = mutableCell(c);
604 if (!data) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cell is outside the hex map", "hexmap"));
605 if (!data->flags.hasRoad()) return Result<void>::success();
606 data->flags = data->flags.without(HexFlags::roadMask());
607 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
608 const HexDirection direction = static_cast<HexDirection>(i);
609 HexCoordinates neighbour{};
610 if (!getNeighbor(c, direction, neighbour)) continue;
611 HexCellData* other = mutableCell(neighbour);
612 other->flags = other->flags.withoutRoad(opposite(direction));
613 refreshCellDependents(neighbour);
614 }
615 refreshCellDependents(c);
616 return Result<void>::success();
617}
618
619// --- brush authoring --------------------------------------------------------
620
621void HexMap::collectBrush(HexCoordinates center, std::int32_t radius, std::vector<std::int32_t>& out) const {
622 out.clear();
623 if (!contains(center) || radius < 0) return;
624 // The brush reaches `radius` steps, so its offset-space footprint is at most
625 // `2 * radius` columns wide and `radius` rows tall. A radius past the grid
626 // diameter already covers everything, and clamping it here keeps the `2 * radius`
627 // footprint (and the loop trip count) inside int32 for script-supplied radii.
628 const std::int64_t diameter = static_cast<std::int64_t>(cellCountX_) + static_cast<std::int64_t>(cellCountZ_);
629 const std::int64_t reach = std::min<std::int64_t>(static_cast<std::int64_t>(radius), diameter);
630 const std::int64_t spread = reach * 2;
631 const std::int32_t centerColumn = center.offsetX();
632 const std::int32_t minColumn =
633 static_cast<std::int32_t>(std::max<std::int64_t>(0, static_cast<std::int64_t>(centerColumn) - spread));
634 const std::int32_t maxColumn = static_cast<std::int32_t>(
635 std::min<std::int64_t>(cellCountX_ - 1, static_cast<std::int64_t>(centerColumn) + spread));
636 const std::int32_t minZ =
637 static_cast<std::int32_t>(std::max<std::int64_t>(0, static_cast<std::int64_t>(center.z) - reach));
638 const std::int32_t maxZ =
639 static_cast<std::int32_t>(std::min<std::int64_t>(cellCountZ_ - 1, static_cast<std::int64_t>(center.z) + reach));
640 for (std::int32_t z = minZ; z <= maxZ; ++z) {
641 for (std::int32_t column = minColumn; column <= maxColumn; ++column) {
643 if (center.distanceTo(candidate) <= radius) out.push_back(column + z * cellCountX_);
644 }
645 }
646}
647
649 if (!contains(center)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex map", "hexmap"));
650 if (radius < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap"));
651 std::vector<std::int32_t> cells;
653 for (std::int32_t index : cells) {
654 const HexCoordinates coordinates = coordinatesAt(index);
655 const std::int32_t next = saturatingAdd(elevation(coordinates), delta);
656 setElevation(coordinates, next).ignore("brush elevation edit clamps per cell");
657 }
658 return Result<void>::success();
659}
660
662 if (!contains(center)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex map", "hexmap"));
663 if (radius < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap"));
664 std::vector<std::int32_t> cells;
666 for (std::int32_t index : cells) {
667 const HexCoordinates coordinates = coordinatesAt(index);
668 setWaterLevel(coordinates, saturatingAdd(waterLevel(coordinates), delta))
669 .ignore("brush water edit clamps per cell");
670 }
671 return Result<void>::success();
672}
673
674Result<void> HexMap::editTerrainType(HexCoordinates center, std::int32_t radius, std::int32_t terrainType) {
675 if (!contains(center)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex map", "hexmap"));
676 if (radius < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap"));
677 std::vector<std::int32_t> cells;
679 for (std::int32_t index : cells) {
680 const HexCoordinates coordinates = coordinatesAt(index);
681 setTerrainType(coordinates, terrainType).ignore("brush terrain edit clamps per cell");
682 }
683 return Result<void>::success();
684}
685
687 std::int32_t delta) {
688 if (!contains(center)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush centre is outside the hex map", "hexmap"));
689 if (radius < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "brush radius must be non-negative", "hexmap"));
690 std::vector<std::int32_t> cells;
692 const std::int32_t layer = layerIndex(feature);
693 for (std::int32_t index : cells) {
694 const HexCoordinates coordinates = coordinatesAt(index);
695 const HexCellData* data = cellAt(index);
696 if (!data || data->flags.hasRiver()) continue;
697 if (data->values.specialIndex() != 0) continue;
698 const std::int32_t current = layer == 0 ? data->values.urbanLevel()
699 : layer == 1 ? data->values.farmLevel()
700 : data->values.plantLevel();
701 const std::int32_t next = clampInt(saturatingAdd(current, delta), 0, 3);
702 if (layer == 0)
703 setUrbanLevel(coordinates, next).ignore("brush urban edit clamps per cell");
704 else if (layer == 1)
705 setFarmLevel(coordinates, next).ignore("brush farm edit clamps per cell");
706 else
707 setPlantLevel(coordinates, next).ignore("brush plant edit clamps per cell");
708 }
709 return Result<void>::success();
710}
711
712} // namespace eve::hexmap
double value
float y
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int mid
Definition AnimSmr.cpp:120
float cx
Definition CardTypes.cpp:33
Stable, structured diagnostics shared by engine modules.
int column
std::map< std::string, Var > values
float u
Definition Grass.cpp:233
std::int32_t waterLevel
Editable hex cell grid: topology, queries, picking and authoring.
std::int32_t c
float elevation
std::array< float, 3 > position
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
int level
graphics::Canvas * previous
TileLayer * layer
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::uint32_t seed
Definition PointSet.cpp:807
float d
float t
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
double current
float dz
float dx
Cell cell
std::map< Cell, int > best
float offsetX
float step
Definition TreeMesh.cpp:314
uint32_t index
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
std::int32_t chunkCount() const noexcept
Total number of chunks.
Definition HexMap.h:102
Result< void > removeRiver(HexCoordinates c)
Removes every river connection of the cell and its neighbours.
Definition HexMap.cpp:558
Result< void > setSpecialIndex(HexCoordinates c, std::int32_t index)
Sets a cell's special-feature index; ignored while the cell carries a river.
Definition HexMap.cpp:464
HexVec3 chunkCenter(std::int32_t chunkIndex) const noexcept
World-space centre of a chunk, at the first cell's elevation.
Definition HexMap.cpp:121
Result< void > editFeatureLevel(HexCoordinates 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).
Definition HexMap.cpp:686
Result< void > setWalled(HexCoordinates c, bool walled)
Sets whether a cell is walled.
Definition HexMap.cpp:474
std::int32_t waterLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:304
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 chunkRowOf(std::int32_t chunkIndex) const noexcept
Chunk row of a chunk index.
Definition HexMap.cpp:111
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
Result< void > editTerrainType(HexCoordinates center, std::int32_t radius, std::int32_t terrainType)
Paints a terrain palette index over a hex brush.
Definition HexMap.cpp:674
std::int32_t chunkColumnOf(std::int32_t chunkIndex) const noexcept
Chunk column of a chunk index.
Definition HexMap.cpp:107
Result< void > setUrbanLevel(HexCoordinates c, std::int32_t level)
Sets a cell's urban level, clamped to [0, 3].
Definition HexMap.cpp:440
std::uint32_t seed() const noexcept
The seed this map was built with.
Definition HexMap.h:106
Result< void > setTerrainType(HexCoordinates c, std::int32_t terrainType)
Sets a cell's terrain palette index, clamped to the palette range.
Definition HexMap.cpp:430
Result< void > removeRoads(HexCoordinates c)
Removes every road of the cell and its neighbours.
Definition HexMap.cpp:602
std::int32_t cellCountX() const noexcept
Number of columns.
Definition HexMap.h:94
void markAllChunksDirty() noexcept
Marks every chunk dirty.
Definition HexMap.cpp:152
HexVec3 cellPosition(HexCoordinates coordinates) const noexcept
Cell centre including elevation and vertical perturbation.
Definition HexMap.cpp:170
std::int32_t terrainType(HexCoordinates c) const noexcept
Definition HexMap.cpp:308
Result< void > setCellState(HexCoordinates c, HexValues values, HexFlags flags)
Writes a complete cell record verbatim.
Definition HexMap.cpp:505
HexValues values(HexCoordinates c) const noexcept
Full packed value record of a cell (zero when outside the grid).
Definition HexMap.cpp:356
bool contains(HexCoordinates coordinates) const noexcept
Whether coordinates address a cell inside the grid.
Definition HexMap.h:122
Result< void > setPlantLevel(HexCoordinates c, std::int32_t level)
Sets a cell's plant level, clamped to [0, 3].
Definition HexMap.cpp:456
Result< void > editWaterLevel(HexCoordinates center, std::int32_t radius, std::int32_t delta)
Applies additive water-level change over a hex brush.
Definition HexMap.cpp:661
Result< HexCoordinates > pickCell(HexVec3 rayOrigin, HexVec3 rayDirection) const noexcept
Finds the cell under a world-space ray.
Definition HexMap.cpp:177
bool getNeighbor(HexCoordinates coordinates, HexDirection direction, HexCoordinates &out) const noexcept
Neighbour of coordinates in direction; false when it is off-grid.
Definition HexMap.cpp:87
std::int32_t specialIndex(HexCoordinates c) const noexcept
Definition HexMap.cpp:324
bool isExplorable(HexCoordinates c) const noexcept
Whether a cell may ever be revealed by a viewer.
Definition HexMap.cpp:352
bool isWalled(HexCoordinates c) const noexcept
Definition HexMap.cpp:344
Result< void > setWaterLevel(HexCoordinates c, std::int32_t waterLevel)
Sets a cell's water level, clamped to [0, maxElevation].
Definition HexMap.cpp:419
Result< void > setOutgoingRiver(HexCoordinates c, HexDirection direction)
Forces a river to leave the cell through direction, mirroring the neighbour.
Definition HexMap.cpp:514
bool isUnderwater(HexCoordinates c) const noexcept
Definition HexMap.cpp:328
bool isExplored(HexCoordinates c) const noexcept
Whether a cell has ever been seen by a viewer (the fog-of-war latch).
Definition HexMap.cpp:348
std::int32_t chunkIndexOf(HexCoordinates coordinates) const noexcept
Chunk index owning coordinates, or -1 when outside the grid.
Definition HexMap.cpp:102
Result< void > reset(std::int32_t cellCountX, std::int32_t cellCountZ, std::uint32_t seed)
Replaces the grid with a new cellCountX * cellCountZ map.
Definition HexMap.cpp:37
HexFlags flags(HexCoordinates c) const noexcept
Full packed flag record of a cell (zero when outside the grid).
Definition HexMap.cpp:360
Result< void > setElevation(HexCoordinates c, std::int32_t elevation)
Sets a cell's elevation, clamped to the editable range, and refreshes dependents.
Definition HexMap.cpp:392
bool hasRiver(HexCoordinates c) const noexcept
Definition HexMap.cpp:332
Result< void > setExplored(HexCoordinates c, bool explored)
Sets the explored (fog-of-war) latch of a cell.
Definition HexMap.cpp:482
std::int32_t cellCountZ() const noexcept
Number of rows.
Definition HexMap.h:96
std::int32_t elevation(HexCoordinates c) const noexcept
Definition HexMap.cpp:300
const HexCellData * cell(HexCoordinates coordinates) const noexcept
Cell record, or null when outside the grid. @ownership Borrowed; the map owns the record....
Definition HexMap.cpp:79
Result< void > setFarmLevel(HexCoordinates c, std::int32_t level)
Sets a cell's farm level, clamped to [0, 3].
Definition HexMap.cpp:448
void collectBrush(HexCoordinates center, std::int32_t radius, std::vector< std::int32_t > &out) const
Enumerates the cells of a hex brush into out (cleared first).
Definition HexMap.cpp:621
HexEdgeType edgeTypeTo(HexCoordinates a, HexCoordinates b) const noexcept
Relationship between two adjacent cells; Flat when not adjacent or out of range.
Definition HexMap.cpp:94
bool hasRiverThrough(HexCoordinates c, HexDirection d) const noexcept
Definition HexMap.cpp:340
void markChunkDirtyAndNeighbors(std::int32_t chunkIndex) noexcept
Marks one chunk and its direct chunk neighbours dirty.
Definition HexMap.cpp:136
Result< void > addRoad(HexCoordinates c, HexDirection direction)
Adds a road through direction when both cells can carry one.
Definition HexMap.cpp:578
std::int32_t urbanLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:312
const HexCellData * cellAt(std::int32_t index) const noexcept
Cell record by linear index, or null when out of range. @ownership Borrowed; the map owns the record....
Definition HexMap.cpp:74
std::int32_t takeDirtyChunk() noexcept
Pops the next dirty chunk.
Definition HexMap.cpp:160
std::int32_t plantLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:320
std::int32_t farmLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:316
Result< void > setExplorable(HexCoordinates c, bool explorable)
Sets whether a cell may ever be explored.
Definition HexMap.cpp:494
bool hasRoad(HexCoordinates c) const noexcept
Definition HexMap.cpp:336
Result< void > editElevation(HexCoordinates center, std::int32_t radius, std::int32_t delta)
Raises or lowers every cell within radius steps of center.
Definition HexMap.cpp:648
HexCoordinates chunkCell(std::int32_t chunkIndex, std::int32_t column, std::int32_t row) const noexcept
Coordinates of the cell at (column, row) inside a chunk.
Definition HexMap.cpp:115
static constexpr float kElevationStep
Vertical distance between two elevation levels.
Definition HexMetrics.h:120
static constexpr float kOuterRadius
Outer (corner) radius of one hex cell in world units.
Definition HexMetrics.h:165
static float elevationY(int elevation) noexcept
Y coordinate of a cell's elevation surface.
Definition HexMetrics.h:265
static constexpr int kChunkSizeZ
Chunk size in the Z dimension.
Definition HexMetrics.h:158
static constexpr int kMinElevation
Minimum editable elevation.
Definition HexMetrics.h:160
static constexpr int kChunkSizeX
Chunk size in the X dimension.
Definition HexMetrics.h:156
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
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 HexValues withElevation(std::int32_t value) const noexcept
Definition HexCell.h:76
constexpr std::int32_t specialIndex() const noexcept
Special index.
Definition HexCell.h:61
constexpr HexValues withWaterLevel(std::int32_t value) const noexcept
With water level.
Definition HexCell.h:80
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
Definition HexMetrics.h:65
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
HexEdgeType
Relationship between two neighbouring cells of different elevation.
Definition HexMetrics.h:93
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
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
Axial coordinates of one hex cell.
static HexVec3 toWorldPosition(HexCoordinates coordinates) noexcept
World-space centre of a cell before elevation and perturbation.
static constexpr HexCoordinates fromOffset(std::int32_t offsetX, std::int32_t offsetZ) noexcept
Converts an odd-row offset pair into axial coordinates.
constexpr HexCoordinates step(HexDirection direction, std::int32_t distance=1) const noexcept
Returns the neighbouring coordinates one step in direction.
static HexCoordinates fromWorldPosition(HexVec3 position) noexcept
Converts a world-space XZ position back to cell coordinates.
Minimal 3-component float vector used by the hex mesh builders.
Definition HexMetrics.h:18
uint32_t pad[2]