载入中...
搜索中...
未找到
Fov.cpp
浏览该文件的文档.
1#include "map/Fov.h"
2
3#include "graphics/Graphics.h"
4#include "graphics/Texture.h"
6
7#include <algorithm>
8#include <cmath>
9#include <cstdint>
10#include <string>
11#include <unordered_set>
12#include <utility>
13#include <vector>
14
15namespace eve::map {
16namespace {
17
18constexpr float kPi = 3.14159265358979323846f;
19constexpr float kExploredMask = 0.35f;
20
21enum class Algorithm : uint8_t { Shadowcast, Raycast, Permissive, Rectangle };
22enum class RadiusMetric : uint8_t { Euclidean, Chebyshev, Manhattan };
23enum class Mode : uint8_t { Grid2D, Heightmap, Volume };
24enum class Topology : uint8_t { Ortho, Hex };
25enum class CellState : uint8_t { Unknown = 0, Explored = 1, Visible = 2 };
26
27Algorithm parseAlgorithm(const std::string &name, Algorithm fallback) {
28 if (name == "shadowcast") return Algorithm::Shadowcast;
29 if (name == "raycast") return Algorithm::Raycast;
30 if (name == "permissive") return Algorithm::Permissive;
31 if (name == "rectangle") return Algorithm::Rectangle;
32 return fallback;
33}
34
35std::string algorithmName(Algorithm a) {
36 switch (a) {
37 case Algorithm::Raycast:
38 return "raycast";
39 case Algorithm::Permissive:
40 return "permissive";
41 case Algorithm::Rectangle:
42 return "rectangle";
43 case Algorithm::Shadowcast:
44 default:
45 return "shadowcast";
46 }
47}
48
49Mode parseMode(const std::string &name, Mode fallback) {
50 if (name == "grid2d") return Mode::Grid2D;
51 if (name == "heightmap") return Mode::Heightmap;
52 if (name == "volume") return Mode::Volume;
53 return fallback;
54}
55
56std::string modeName(Mode m) {
57 switch (m) {
58 case Mode::Heightmap:
59 return "heightmap";
60 case Mode::Volume:
61 return "volume";
62 case Mode::Grid2D:
63 default:
64 return "grid2d";
65 }
66}
67
68RadiusMetric parseMetric(const std::string &name, RadiusMetric fallback) {
69 if (name == "euclidean") return RadiusMetric::Euclidean;
70 if (name == "chebyshev") return RadiusMetric::Chebyshev;
71 if (name == "manhattan") return RadiusMetric::Manhattan;
72 return fallback;
73}
74
75std::string metricName(RadiusMetric m) {
76 switch (m) {
77 case RadiusMetric::Euclidean:
78 return "euclidean";
79 case RadiusMetric::Chebyshev:
80 return "chebyshev";
81 case RadiusMetric::Manhattan:
82 return "manhattan";
83 }
84 return "euclidean";
85}
86
87std::string topologyName(Topology t) { return t == Topology::Hex ? "hex" : "ortho"; }
88
89void offsetToCube(int x, int y, int &q, int &r, int &s) {
90 // odd-r staggered (matches Pathfinder hex)
91 q = x - (y - (y & 1)) / 2;
92 r = y;
93 s = -q - r;
94}
95
96void cubeToOffset(int q, int r, int &x, int &y) {
97 y = r;
98 x = q + (r - (r & 1)) / 2;
99}
100
101int cubeDistance(int q0, int r0, int s0, int q1, int r1, int s1) {
102 return std::max({std::abs(q0 - q1), std::abs(r0 - r1), std::abs(s0 - s1)});
103}
104
105int hexDistance(int x0, int y0, int x1, int y1) {
106 int q0, r0, s0, q1, r1, s1;
107 offsetToCube(x0, y0, q0, r0, s0);
108 offsetToCube(x1, y1, q1, r1, s1);
109 return cubeDistance(q0, r0, s0, q1, r1, s1);
110}
111
112bool inRadiusOrtho(RadiusMetric metric, int dx, int dy, int radius) {
113 if (radius < 0) return false;
114 const int adx = std::abs(dx);
115 const int ady = std::abs(dy);
116 switch (metric) {
117 case RadiusMetric::Chebyshev:
118 return std::max(adx, ady) <= radius;
119 case RadiusMetric::Manhattan:
120 return adx + ady <= radius;
121 case RadiusMetric::Euclidean:
122 default:
123 return adx * adx + ady * ady <= radius * radius;
124 }
125}
126
127bool inRadius3Ortho(RadiusMetric metric, int dx, int dy, int dz, int radius) {
128 if (radius < 0) return false;
129 const int adx = std::abs(dx);
130 const int ady = std::abs(dy);
131 const int adz = std::abs(dz);
132 switch (metric) {
133 case RadiusMetric::Chebyshev:
134 return std::max({adx, ady, adz}) <= radius;
135 case RadiusMetric::Manhattan:
136 return adx + ady + adz <= radius;
137 case RadiusMetric::Euclidean:
138 default:
139 return adx * adx + ady * ady + adz * adz <= radius * radius;
140 }
141}
142
143float angleDiffDeg(float a, float b) {
144 float d = a - b;
145 while (d > 180.f) d -= 360.f;
146 while (d < -180.f) d += 360.f;
147 return d;
148}
149
150bool inCone(int ox, int oy, int x, int y, bool useCone, float facingDeg, float halfAngleDeg) {
151 if (!useCone || halfAngleDeg >= 180.f) return true;
152 if (x == ox && y == oy) return true;
153 const float ang = std::atan2(float(y - oy), float(x - ox)) * (180.f / kPi);
154 return std::fabs(angleDiffDeg(ang, facingDeg)) <= halfAngleDeg;
155}
156
157uint8_t stateToMaskByte(CellState s) {
158 switch (s) {
159 case CellState::Visible:
160 return 255;
161 case CellState::Explored:
162 return uint8_t(kExploredMask * 255.f + 0.5f);
163 case CellState::Unknown:
164 default:
165 return 0;
166 }
167}
168
169float stateToMaskValue(CellState s) {
170 switch (s) {
171 case CellState::Visible:
172 return 1.f;
173 case CellState::Explored:
174 return kExploredMask;
175 case CellState::Unknown:
176 default:
177 return 0.f;
178 }
179}
180
181float normalizeAngle(float a) {
182 while (a <= -kPi) a += 2.f * kPi;
183 while (a > kPi) a -= 2.f * kPi;
184 return a;
185}
186
187constexpr int kMultXX[8] = {1, 0, 0, -1, -1, 0, 0, 1};
188constexpr int kMultXY[8] = {0, 1, -1, 0, 0, -1, 1, 0};
189constexpr int kMultYX[8] = {0, 1, 1, 0, 0, -1, -1, 0};
190constexpr int kMultYY[8] = {1, 0, 0, 1, -1, 0, 0, -1};
191
192constexpr int kCubeDirs[6][3] = {{1, -1, 0}, {1, 0, -1}, {0, 1, -1}, {-1, 1, 0}, {-1, 0, 1}, {0, -1, 1}};
193
194} // namespace
195
196struct Fov::Impl {
197 struct Revealer {
198 int id = 0;
199 int x = 0;
200 int y = 0;
201 int z = 0;
202 int radius = 0;
203 bool enabled = true;
204 bool useCone = false;
205 float facingDeg = 0.f;
206 float halfAngleDeg = 180.f;
207 float perception = 0.f;
208 };
209
210 struct Rect {
211 int x0, y0, x1, y1;
212 };
213
214 struct AngleShadow {
215 float start;
216 float end;
217 };
218
219 int width = 0;
220 int height = 0;
221 int depth = 1;
222 TileLayer *layer = nullptr;
223 uint64_t layerRevision = 0;
224 Algorithm algorithm = Algorithm::Shadowcast;
225 RadiusMetric metric = RadiusMetric::Euclidean;
226 Mode mode = Mode::Grid2D;
227 Topology topology = Topology::Ortho;
228 bool topologyManual = false;
229 bool cornerPeek = false;
230 bool blockEmpty = true;
231 bool dirty = true;
232 float cliffBlock = 1.f;
233 float eyeOffset = 0.f;
236 float detectionMargin = 0.f;
237
238 std::vector<uint8_t> opaque;
239 std::vector<float> elevation;
240 std::vector<CellState> state;
241 std::vector<int> visibleList;
242 std::unordered_set<uint32_t> opaqueGids;
243 std::vector<Revealer> revealers;
245
246 int index2(int x, int y) const { return y * width + x; }
247 int index3(int x, int y, int z) const { return (z * height + y) * width + x; }
248 bool inBounds2(int x, int y) const { return x >= 0 && y >= 0 && x < width && y < height; }
249 bool inBounds3(int x, int y, int z) const {
250 return inBounds2(x, y) && z >= 0 && z < depth;
251 }
252
254 if (verticalRange < 0) return std::max(0, depth);
255 return verticalRange;
256 }
257
258 int effectiveRadiusOf(const Revealer &r) const {
259 const int bonus = int(std::floor(r.perception * perceptionRadiusScale));
260 return std::max(0, r.radius + bonus);
261 }
262
263 bool inRadius2(int ox, int oy, int x, int y, int radius) const {
264 if (topology == Topology::Hex) return hexDistance(ox, oy, x, y) <= radius;
265 return inRadiusOrtho(metric, x - ox, y - oy, radius);
266 }
267
268 bool inRadius3(int ox, int oy, int oz, int x, int y, int z, int radius) const {
269 if (topology == Topology::Hex) {
270 return hexDistance(ox, oy, x, y) + std::abs(z - oz) <= radius;
271 }
272 return inRadius3Ortho(metric, x - ox, y - oy, z - oz, radius);
273 }
274
276 if (!layer || topologyManual) return;
277 const auto o = layer->config()->orientation;
278 topology = (o == MapOrientation::Hexagonal || o == MapOrientation::Staggered) ? Topology::Hex
279 : Topology::Ortho;
280 }
281
282 void resize(int w, int h, int d) {
283 width = w > 0 ? w : 0;
284 height = h > 0 ? h : 0;
285 depth = d > 0 ? d : 1;
286 const size_t n = size_t(width * height * depth);
287 opaque.assign(n, 0);
288 state.assign(n, CellState::Unknown);
289 elevation.assign(size_t(width * height), 0.f);
290 visibleList.clear();
291 dirty = true;
292 }
293
295 layer = l;
296 if (!layer) return;
297 auto cfg = layer->config();
298 resize(cfg->mapW, cfg->mapH, 1);
299 if (mode == Mode::Volume) mode = Mode::Grid2D;
302 }
303
305 if (!layer) return;
306 auto cfg = layer->config();
307 auto tiles = layer->tiles();
308 auto tileset = layer->tileset();
309 if (cfg->mapW != width || cfg->mapH != height || depth != 1) {
310 resize(cfg->mapW, cfg->mapH, 1);
311 }
313 const int n = width * height;
314 for (int i = 0; i < n; ++i) {
315 const uint32_t gid = (i < int(tiles->gids.size())) ? tileGid(tiles->gids[size_t(i)]) : 0u;
316 bool isOpaque = false;
317 if (blockEmpty && gid == 0u) isOpaque = true;
318 if (opaqueGids.count(gid)) isOpaque = true;
319 const auto visual =
320 std::find_if(tileset->visuals.begin(), tileset->visuals.end(),
321 [gid](const TileLayer::Tileset::Visual &candidate) { return candidate.gid == int(gid); });
322 if (visual != tileset->visuals.end()) isOpaque = isOpaque || visual->opaque;
323 opaque[size_t(i)] = isOpaque ? 1u : 0u;
324 }
325 layerRevision = tiles->revision;
326 dirty = true;
327 }
328
329 bool cellOpaque2(int x, int y) const {
330 if (!inBounds2(x, y)) return true;
331 return opaque[size_t(index2(x, y))] != 0;
332 }
333
334 bool cellOpaque3(int x, int y, int z) const {
335 if (!inBounds3(x, y, z)) return true;
336 return opaque[size_t(index3(x, y, z))] != 0;
337 }
338
339 bool cellOpaqueOnSlice(int x, int y, int zSlice) const {
340 if (mode == Mode::Volume) return cellOpaque3(x, y, zSlice);
341 return cellOpaque2(x, y);
342 }
343
344 float elevAt(int x, int y) const {
345 if (!inBounds2(x, y)) return 0.f;
346 return elevation[size_t(index2(x, y))];
347 }
348
349 void markVisibleIdx(int idx) {
350 if (idx < 0 || idx >= int(state.size())) return;
351 if (state[size_t(idx)] != CellState::Visible) {
352 state[size_t(idx)] = CellState::Visible;
353 visibleList.push_back(idx);
354 }
355 }
356
357 void markVisible2(int x, int y) {
358 if (!inBounds2(x, y)) return;
360 }
361
362 void markVisible3(int x, int y, int z) {
363 if (!inBounds3(x, y, z)) return;
365 }
366
368 for (int idx : visibleList) {
369 if (idx >= 0 && idx < int(state.size()) && state[size_t(idx)] == CellState::Visible) {
370 state[size_t(idx)] = CellState::Explored;
371 }
372 }
373 visibleList.clear();
374 }
375
377 std::fill(state.begin(), state.end(), CellState::Unknown);
378 visibleList.clear();
379 dirty = true;
380 }
381
383 for (auto &r : revealers) {
384 if (r.id == id) return &r;
385 }
386 return nullptr;
387 }
388
389 const Revealer *findRevealerConst(int id) const {
390 for (const auto &r : revealers) {
391 if (r.id == id) return &r;
392 }
393 return nullptr;
394 }
395
396 bool losBresenham2(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const {
397 int dx = std::abs(x1 - x0);
398 int dy = std::abs(y1 - y0);
399 const int sx = x0 < x1 ? 1 : -1;
400 const int sy = y0 < y1 ? 1 : -1;
401 int err = dx - dy;
402 int x = x0;
403 int y = y0;
404 const float viewerElev = elevAt(x0, y0) + eyeOffset;
405
406 while (true) {
407 if (x == x1 && y == y1) return true;
408 const int e2 = 2 * err;
409 int nx = x;
410 int ny = y;
411 if (e2 > -dy) {
412 err -= dy;
413 nx += sx;
414 }
415 if (e2 < dx) {
416 err += dx;
417 ny += sy;
418 }
419 const bool atEnd = (nx == x1 && ny == y1);
420 if (!atEnd && cellOpaqueOnSlice(nx, ny, zSlice)) return false;
421 if (applyHeight && mode == Mode::Heightmap && !atEnd) {
422 if (elevAt(nx, ny) >= viewerElev + cliffBlock) return false;
423 }
424 if (!cornerPeek && nx != x && ny != y) {
425 if (cellOpaqueOnSlice(nx, y, zSlice) && cellOpaqueOnSlice(x, ny, zSlice)) {
426 return false;
427 }
428 }
429 x = nx;
430 y = ny;
431 }
432 }
433
434 bool losHexCube(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const {
435 int q0, r0, s0, q1, r1, s1;
436 offsetToCube(x0, y0, q0, r0, s0);
437 offsetToCube(x1, y1, q1, r1, s1);
438 const int n = cubeDistance(q0, r0, s0, q1, r1, s1);
439 if (n == 0) return true;
440 const float viewerElev = elevAt(x0, y0) + eyeOffset;
441 for (int i = 1; i <= n; ++i) {
442 const float t = float(i) / float(n);
443 const float qf = float(q0) + (float(q1) - float(q0)) * t;
444 const float rf = float(r0) + (float(r1) - float(r0)) * t;
445 const float sf = float(s0) + (float(s1) - float(s0)) * t;
446 // cube round
447 int rq = int(std::lround(qf));
448 int rr = int(std::lround(rf));
449 int rs = int(std::lround(sf));
450 const float qdiff = std::fabs(rq - qf);
451 const float rdiff = std::fabs(rr - rf);
452 const float sdiff = std::fabs(rs - sf);
453 if (qdiff > rdiff && qdiff > sdiff) rq = -rr - rs;
454 else if (rdiff > sdiff) rr = -rq - rs;
455 else rs = -rq - rr;
456 (void)rs;
457 int x, y;
458 cubeToOffset(rq, rr, x, y);
459 const bool atEnd = (i == n);
460 if (!atEnd && cellOpaqueOnSlice(x, y, zSlice)) return false;
461 if (applyHeight && mode == Mode::Heightmap && !atEnd) {
462 if (elevAt(x, y) >= viewerElev + cliffBlock) return false;
463 }
464 }
465 return true;
466 }
467
468 bool los2(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const {
469 if (topology == Topology::Hex) return losHexCube(x0, y0, x1, y1, zSlice, applyHeight);
470 return losBresenham2(x0, y0, x1, y1, zSlice, applyHeight);
471 }
472
473 bool passesHeightToTarget(int ox, int oy, int tx, int ty) const {
474 if (mode != Mode::Heightmap) return true;
475 return los2(ox, oy, tx, ty, 0, true);
476 }
477
478 void tryMark2(int ox, int oy, int x, int y, int zSlice, bool useCone, float facingDeg,
479 float halfAngleDeg, int radius) {
480 if (!inBounds2(x, y)) return;
481 if (!inRadius2(ox, oy, x, y, radius)) return;
482 if (!inCone(ox, oy, x, y, useCone, facingDeg, halfAngleDeg)) return;
483 if (!passesHeightToTarget(ox, oy, x, y)) return;
484 if (mode == Mode::Volume) markVisible3(x, y, zSlice);
485 else markVisible2(x, y);
486 }
487
488 void castLight(int ox, int oy, int radius, int row, float startSlope, float endSlope, int xx,
489 int xy, int yx, int yy, int zSlice, bool useCone, float facingDeg,
490 float halfAngleDeg) {
491 if (startSlope < endSlope) return;
492 float newStart = 0.f;
493 for (int j = row; j <= radius; ++j) {
494 int dx = -j - 1;
495 int dy = -j;
496 bool blocked = false;
497 while (dx <= 0) {
498 ++dx;
499 const int mapX = ox + dx * xx + dy * xy;
500 const int mapY = oy + dx * yx + dy * yy;
501 const float leftSlope = (float(dx) - 0.5f) / (float(dy) + 0.5f);
502 const float rightSlope = (float(dx) + 0.5f) / (float(dy) - 0.5f);
503 if (startSlope < rightSlope) continue;
504 if (endSlope > leftSlope) break;
505 tryMark2(ox, oy, mapX, mapY, zSlice, useCone, facingDeg, halfAngleDeg, radius);
506 if (blocked) {
507 if (cellOpaqueOnSlice(mapX, mapY, zSlice)) {
508 newStart = rightSlope;
509 continue;
510 }
511 blocked = false;
512 startSlope = newStart;
513 } else if (cellOpaqueOnSlice(mapX, mapY, zSlice) && j < radius) {
514 blocked = true;
515 castLight(ox, oy, radius, j + 1, startSlope, leftSlope, xx, xy, yx, yy, zSlice,
516 useCone, facingDeg, halfAngleDeg);
517 newStart = rightSlope;
518 }
519 }
520 if (blocked) break;
521 }
522 }
523
524 void computeOrthoShadowcast(const Revealer &r, int zSlice, int radius) {
525 tryMark2(r.x, r.y, r.x, r.y, zSlice, r.useCone, r.facingDeg, r.halfAngleDeg, radius);
526 if (radius == 0) return;
527 for (int oct = 0; oct < 8; ++oct) {
528 castLight(r.x, r.y, radius, 1, 1.f, 0.f, kMultXX[oct], kMultXY[oct], kMultYX[oct],
529 kMultYY[oct], zSlice, r.useCone, r.facingDeg, r.halfAngleDeg);
530 }
531 }
532
533 float hexCellAngle(int ox, int oy, int x, int y) const {
534 const float px = float(x) + ((y & 1) ? 0.5f : 0.f);
535 const float py = float(y) * 0.86602540378f; // ≈ √3/2
536 const float opx = float(ox) + ((oy & 1) ? 0.5f : 0.f);
537 const float opy = float(oy) * 0.86602540378f;
538 return std::atan2(py - opy, px - opx);
539 }
540
541 bool angleFullyCovered(float a0, float a1, const std::vector<AngleShadow> &shadows) const {
542 // Conservative: sample mid angle; for small cells this is enough for FOV.
543 float mid = normalizeAngle(0.5f * (a0 + a1));
544 // handle wrap when a0/a1 straddle ±π
545 if (std::fabs(a1 - a0) > kPi) mid = normalizeAngle(mid + kPi);
546 for (const auto &sh : shadows) {
547 float s = sh.start, e = sh.end;
548 if (s <= e) {
549 if (mid >= s && mid <= e) return true;
550 } else {
551 if (mid >= s || mid <= e) return true;
552 }
553 }
554 return false;
555 }
556
557 void addAngleShadow(std::vector<AngleShadow> &shadows, float a0, float a1) const {
558 a0 = normalizeAngle(a0);
559 a1 = normalizeAngle(a1);
560 // Ensure [start,end] covers the short arc from a0 to a1 going the opaque wedge way:
561 // use unordered span expanded slightly.
562 float start = a0;
563 float end = a1;
564 float diff = normalizeAngle(end - start);
565 if (diff < 0.f) std::swap(start, end);
566 shadows.push_back(AngleShadow{normalizeAngle(start), normalizeAngle(end)});
567 }
568
569 void computeHexShadowcast(const Revealer &r, int zSlice, int radius) {
570 tryMark2(r.x, r.y, r.x, r.y, zSlice, r.useCone, r.facingDeg, r.halfAngleDeg, radius);
571 if (radius == 0) return;
572 std::vector<AngleShadow> shadows;
573 shadows.reserve(64);
574
575 int cq, cr, cs;
576 offsetToCube(r.x, r.y, cq, cr, cs);
577
578 for (int ring = 1; ring <= radius; ++ring) {
579 // Start at cube +dir0 * ring, walk 6 edges
580 int q = cq + kCubeDirs[4][0] * ring;
581 int rr = cr + kCubeDirs[4][1] * ring;
582 int s = cs + kCubeDirs[4][2] * ring;
583 for (int side = 0; side < 6; ++side) {
584 for (int step = 0; step < ring; ++step) {
585 int x, y;
586 cubeToOffset(q, rr, x, y);
587 if (inBounds2(x, y) && inRadius2(r.x, r.y, x, y, radius) &&
588 inCone(r.x, r.y, x, y, r.useCone, r.facingDeg, r.halfAngleDeg)) {
589 const float ang = hexCellAngle(r.x, r.y, x, y);
590 const float half =
591 (ring <= 0) ? kPi : (0.55f / float(ring)); // angular half-width
592 const float a0 = normalizeAngle(ang - half);
593 const float a1 = normalizeAngle(ang + half);
594 if (!angleFullyCovered(a0, a1, shadows) &&
595 passesHeightToTarget(r.x, r.y, x, y)) {
596 tryMark2(r.x, r.y, x, y, zSlice, false, 0.f, 180.f, radius);
597 }
598 if (cellOpaqueOnSlice(x, y, zSlice)) {
599 addAngleShadow(shadows, a0, a1);
600 }
601 }
602 q += kCubeDirs[side][0];
603 rr += kCubeDirs[side][1];
604 s += kCubeDirs[side][2];
605 (void)s;
606 }
607 }
608 }
609 }
610
611 void computeShadowcast(const Revealer &r, int zSlice, int radius) {
612 if (topology == Topology::Hex) computeHexShadowcast(r, zSlice, radius);
613 else computeOrthoShadowcast(r, zSlice, radius);
614 }
615
616 void castRayLine(int ox, int oy, int tx, int ty, int zSlice, bool useCone, float facingDeg,
617 float halfAngleDeg, int radius) {
618 if (topology == Topology::Hex) {
619 // Follow cube line, marking until blocked.
620 int q0, r0, s0, q1, r1, s1;
621 offsetToCube(ox, oy, q0, r0, s0);
622 offsetToCube(tx, ty, q1, r1, s1);
623 const int n = std::max(1, cubeDistance(q0, r0, s0, q1, r1, s1));
624 for (int i = 0; i <= n; ++i) {
625 const float t = float(i) / float(n);
626 int rq = int(std::lround(float(q0) + (float(q1) - float(q0)) * t));
627 int rr = int(std::lround(float(r0) + (float(r1) - float(r0)) * t));
628 int rs = -rq - rr;
629 (void)rs;
630 int x, y;
631 cubeToOffset(rq, rr, x, y);
632 tryMark2(ox, oy, x, y, zSlice, useCone, facingDeg, halfAngleDeg, radius);
633 if (i > 0 && cellOpaqueOnSlice(x, y, zSlice)) break;
634 }
635 return;
636 }
637 int dx = std::abs(tx - ox);
638 int dy = std::abs(ty - oy);
639 const int sx = ox < tx ? 1 : -1;
640 const int sy = oy < ty ? 1 : -1;
641 int err = dx - dy;
642 int x = ox;
643 int y = oy;
644 while (true) {
645 tryMark2(ox, oy, x, y, zSlice, useCone, facingDeg, halfAngleDeg, radius);
646 if (x == tx && y == ty) break;
647 if (!(x == ox && y == oy) && cellOpaqueOnSlice(x, y, zSlice)) break;
648 const int e2 = 2 * err;
649 if (e2 > -dy) {
650 err -= dy;
651 x += sx;
652 }
653 if (e2 < dx) {
654 err += dx;
655 y += sy;
656 }
657 }
658 }
659
660 void computeRaycast(const Revealer &r, int zSlice, int radius) {
661 tryMark2(r.x, r.y, r.x, r.y, zSlice, r.useCone, r.facingDeg, r.halfAngleDeg, radius);
662 if (radius == 0) return;
663 if (topology == Topology::Hex) {
664 int cq, cr, cs;
665 offsetToCube(r.x, r.y, cq, cr, cs);
666 for (int ring = 1; ring <= radius; ++ring) {
667 int q = cq + kCubeDirs[4][0] * ring;
668 int rr = cr + kCubeDirs[4][1] * ring;
669 for (int side = 0; side < 6; ++side) {
670 for (int step = 0; step < ring; ++step) {
671 int x, y;
672 cubeToOffset(q, rr, x, y);
673 castRayLine(r.x, r.y, x, y, zSlice, r.useCone, r.facingDeg, r.halfAngleDeg,
674 radius);
675 q += kCubeDirs[side][0];
676 rr += kCubeDirs[side][1];
677 }
678 }
679 }
680 return;
681 }
682 for (int i = -radius; i <= radius; ++i) {
683 castRayLine(r.x, r.y, r.x + i, r.y - radius, zSlice, r.useCone, r.facingDeg,
684 r.halfAngleDeg, radius);
685 castRayLine(r.x, r.y, r.x + i, r.y + radius, zSlice, r.useCone, r.facingDeg,
686 r.halfAngleDeg, radius);
687 castRayLine(r.x, r.y, r.x - radius, r.y + i, zSlice, r.useCone, r.facingDeg,
688 r.halfAngleDeg, radius);
689 castRayLine(r.x, r.y, r.x + radius, r.y + i, zSlice, r.useCone, r.facingDeg,
690 r.halfAngleDeg, radius);
691 }
692 }
693
694 void computePermissive(const Revealer &r, int zSlice, int radius) {
695 tryMark2(r.x, r.y, r.x, r.y, zSlice, r.useCone, r.facingDeg, r.halfAngleDeg, radius);
696 if (radius == 0) return;
697 for (int dy = -radius; dy <= radius; ++dy) {
698 for (int dx = -radius; dx <= radius; ++dx) {
699 if (dx == 0 && dy == 0) continue;
700 const int tx = r.x + dx;
701 const int ty = r.y + dy;
702 if (!inBounds2(tx, ty)) continue;
703 if (!inRadius2(r.x, r.y, tx, ty, radius)) continue;
704 if (!inCone(r.x, r.y, tx, ty, r.useCone, r.facingDeg, r.halfAngleDeg)) continue;
705 bool see = los2(r.x, r.y, tx, ty, zSlice, mode == Mode::Heightmap);
706 if (!see) {
707 const int sx = (dx > 0) ? -1 : (dx < 0 ? 1 : 0);
708 const int sy = (dy > 0) ? -1 : (dy < 0 ? 1 : 0);
709 const int cands[3][2] = {{tx + sx, ty}, {tx, ty + sy}, {tx + sx, ty + sy}};
710 for (auto &c : cands) {
711 const int ax = c[0];
712 const int ay = c[1];
713 if (ax == tx && ay == ty) continue;
714 if (!inBounds2(ax, ay)) continue;
715 if (cellOpaqueOnSlice(ax, ay, zSlice)) continue;
716 if (!inRadius2(r.x, r.y, ax, ay, radius)) continue;
717 if (los2(r.x, r.y, ax, ay, zSlice, mode == Mode::Heightmap)) {
718 see = true;
719 break;
720 }
721 }
722 }
723 if (see) tryMark2(r.x, r.y, tx, ty, zSlice, false, 0.f, 180.f, radius);
724 }
725 }
726 }
727
728 std::vector<Rect> buildOpaqueRects(int ox, int oy, int radius, int zSlice) const {
729 const int xMin = std::max(0, ox - radius);
730 const int xMax = std::min(width - 1, ox + radius);
731 const int yMin = std::max(0, oy - radius);
732 const int yMax = std::min(height - 1, oy + radius);
733 const int bw = xMax - xMin + 1;
734 const int bh = yMax - yMin + 1;
735 std::vector<uint8_t> used(size_t(bw * bh), 0);
736 auto usedAt = [&](int x, int y) -> uint8_t & {
737 return used[size_t((y - yMin) * bw + (x - xMin))];
738 };
739
740 std::vector<Rect> rects;
741 for (int y = yMin; y <= yMax; ++y) {
742 for (int x = xMin; x <= xMax; ++x) {
743 if (usedAt(x, y)) continue;
744 if (!cellOpaqueOnSlice(x, y, zSlice)) continue;
745 if (!inRadius2(ox, oy, x, y, radius)) continue;
746 int x1 = x;
747 while (x1 + 1 <= xMax && !usedAt(x1 + 1, y) && cellOpaqueOnSlice(x1 + 1, y, zSlice) &&
748 inRadius2(ox, oy, x1 + 1, y, radius)) {
749 ++x1;
750 }
751 int y1 = y;
752 bool grow = true;
753 while (grow && y1 + 1 <= yMax) {
754 for (int xx = x; xx <= x1; ++xx) {
755 if (usedAt(xx, y1 + 1) || !cellOpaqueOnSlice(xx, y1 + 1, zSlice) ||
756 !inRadius2(ox, oy, xx, y1 + 1, radius)) {
757 grow = false;
758 break;
759 }
760 }
761 if (grow) ++y1;
762 }
763 for (int yy = y; yy <= y1; ++yy)
764 for (int xx = x; xx <= x1; ++xx) usedAt(xx, yy) = 1;
765 rects.push_back(Rect{x, y, x1, y1});
766 }
767 }
768 return rects;
769 }
770
771 void computeRectangle(const Revealer &r, int zSlice, int radius) {
772 // Mark all in-radius cells, then carve umbras behind opaque rectangles.
773 std::vector<uint8_t> lit(size_t(width * height), 0);
774 auto litAt = [&](int x, int y) -> uint8_t & { return lit[size_t(index2(x, y))]; };
775
776 for (int y = std::max(0, r.y - radius); y <= std::min(height - 1, r.y + radius); ++y) {
777 for (int x = std::max(0, r.x - radius); x <= std::min(width - 1, r.x + radius); ++x) {
778 if (!inRadius2(r.x, r.y, x, y, radius)) continue;
779 if (!inCone(r.x, r.y, x, y, r.useCone, r.facingDeg, r.halfAngleDeg)) continue;
780 if (!passesHeightToTarget(r.x, r.y, x, y)) continue;
781 litAt(x, y) = 1;
782 }
783 }
784
785 auto rects = buildOpaqueRects(r.x, r.y, radius, zSlice);
786 const float ox = float(r.x) + 0.5f;
787 const float oy = float(r.y) + 0.5f;
788 std::sort(rects.begin(), rects.end(), [&](const Rect &a, const Rect &b) {
789 const int acx = (a.x0 + a.x1) / 2;
790 const int acy = (a.y0 + a.y1) / 2;
791 const int bcx = (b.x0 + b.x1) / 2;
792 const int bcy = (b.y0 + b.y1) / 2;
793 return inRadius2(r.x, r.y, acx, acy, radius) &&
794 (std::abs(acx - r.x) + std::abs(acy - r.y)) <
795 (std::abs(bcx - r.x) + std::abs(bcy - r.y));
796 });
797
798 for (const auto &rc : rects) {
799 // Tangents from origin to rectangle corners → umbra angles.
800 const float corners[4][2] = {
801 {float(rc.x0), float(rc.y0)},
802 {float(rc.x1) + 1.f, float(rc.y0)},
803 {float(rc.x0), float(rc.y1) + 1.f},
804 {float(rc.x1) + 1.f, float(rc.y1) + 1.f},
805 };
806 float angles[4];
807 for (int i = 0; i < 4; ++i) {
808 angles[i] = std::atan2(corners[i][1] - oy, corners[i][0] - ox);
809 }
810 float aMin = angles[0], aMax = angles[0];
811 for (int i = 1; i < 4; ++i) {
812 const float dMin = normalizeAngle(angles[i] - aMin);
813 const float dMax = normalizeAngle(angles[i] - aMax);
814 if (dMin < 0.f) aMin = angles[i];
815 if (dMax > 0.f) aMax = angles[i];
816 }
817 const float nearest2 =
818 float(std::min({(rc.x0 - r.x) * (rc.x0 - r.x) + (rc.y0 - r.y) * (rc.y0 - r.y),
819 (rc.x1 - r.x) * (rc.x1 - r.x) + (rc.y0 - r.y) * (rc.y0 - r.y),
820 (rc.x0 - r.x) * (rc.x0 - r.x) + (rc.y1 - r.y) * (rc.y1 - r.y),
821 (rc.x1 - r.x) * (rc.x1 - r.x) + (rc.y1 - r.y) * (rc.y1 - r.y)}));
822
823 for (int y = std::max(0, r.y - radius); y <= std::min(height - 1, r.y + radius); ++y) {
824 for (int x = std::max(0, r.x - radius); x <= std::min(width - 1, r.x + radius); ++x) {
825 if (!litAt(x, y)) continue;
826 // Keep the blocking rectangle itself lit.
827 if (x >= rc.x0 && x <= rc.x1 && y >= rc.y0 && y <= rc.y1) continue;
828 const float d2 = float((x - r.x) * (x - r.x) + (y - r.y) * (y - r.y));
829 if (d2 <= nearest2 + 0.01f) continue;
830 const float ang = std::atan2(float(y) + 0.5f - oy, float(x) + 0.5f - ox);
831 bool inside = false;
832 if (aMin <= aMax) inside = (ang >= aMin && ang <= aMax);
833 else inside = (ang >= aMin || ang <= aMax);
834 // Use normalized compare for wrapped wedges.
835 if (normalizeAngle(aMax - aMin) < 0.f) {
836 inside = normalizeAngle(ang - aMin) >= 0.f ||
837 normalizeAngle(aMax - ang) >= 0.f;
838 } else {
839 const float da = normalizeAngle(ang - aMin);
840 const float span = normalizeAngle(aMax - aMin);
841 inside = da >= 0.f && da <= span;
842 }
843 if (inside) litAt(x, y) = 0;
844 }
845 }
846 }
847
848 for (int y = 0; y < height; ++y) {
849 for (int x = 0; x < width; ++x) {
850 if (litAt(x, y)) tryMark2(r.x, r.y, x, y, zSlice, false, 0.f, 180.f, radius);
851 }
852 }
853 }
854
855 void extendVertical(const Revealer &r, int zSlice, int radius) {
856 if (mode != Mode::Volume) return;
857 const int vRange = effectiveVerticalRange();
858 for (int y = std::max(0, r.y - radius); y <= std::min(height - 1, r.y + radius); ++y) {
859 for (int x = std::max(0, r.x - radius); x <= std::min(width - 1, r.x + radius); ++x) {
860 const int idx = index3(x, y, zSlice);
861 if (state[size_t(idx)] != CellState::Visible) continue;
862 for (int dir = -1; dir <= 1; dir += 2) {
863 for (int step = 1; step <= vRange; ++step) {
864 const int z = zSlice + dir * step;
865 if (z < 0 || z >= depth) break;
866 if (!inRadius3(r.x, r.y, r.z, x, y, z, radius)) break;
867 markVisible3(x, y, z);
868 if (cellOpaque3(x, y, z)) break;
869 }
870 }
871 }
872 }
873 }
874
876 if (!r.enabled) return;
877 if (!inBounds2(r.x, r.y)) return;
878 int zSlice = 0;
879 if (mode == Mode::Volume) {
880 if (!inBounds3(r.x, r.y, r.z)) return;
881 zSlice = r.z;
882 }
883 const int radius = effectiveRadiusOf(r);
884
885 switch (algorithm) {
886 case Algorithm::Raycast:
887 computeRaycast(r, zSlice, radius);
888 break;
889 case Algorithm::Permissive:
890 computePermissive(r, zSlice, radius);
891 break;
892 case Algorithm::Rectangle:
893 computeRectangle(r, zSlice, radius);
894 break;
895 case Algorithm::Shadowcast:
896 default:
897 computeShadowcast(r, zSlice, radius);
898 break;
899 }
900 extendVertical(r, zSlice, radius);
901 }
902
903 void compute() {
904 if (layer && layerRevision != layer->tiles()->revision) syncFromLayer();
905 if (!dirty) return;
907 for (const auto &r : revealers) computeRevealer(r);
908 dirty = false;
909 }
910};
911
912Fov::Fov() : impl_(std::make_unique<Impl>()) {}
916Fov::~Fov() = default;
917Fov::Fov(Fov &&) noexcept = default;
918Fov &Fov::operator=(Fov &&) noexcept = default;
919
920void Fov::bindLayer(TileLayer *layer) {
921 if (!layer) return;
922 impl_->bindLayer(layer);
923}
924
925void Fov::setSize(int width, int height) {
926 impl_->layer = nullptr;
927 if (impl_->mode == Mode::Volume) impl_->mode = Mode::Grid2D;
928 impl_->resize(width, height, 1);
929}
930
932 impl_->layer = nullptr;
933 impl_->mode = Mode::Volume;
934 impl_->resize(width, height, depth);
935}
936
937int Fov::getWidth() const { return impl_->width; }
938int Fov::getHeight() const { return impl_->height; }
939int Fov::getDepth() const { return impl_->depth; }
940
941void Fov::setMode(const std::string &name) {
942 const Mode next = parseMode(name, impl_->mode);
943 impl_->mode = next;
944 if (next != Mode::Volume && impl_->depth != 1) {
945 impl_->resize(impl_->width, impl_->height, 1);
946 }
947 impl_->dirty = true;
948}
949
950std::string Fov::getMode() const { return modeName(impl_->mode); }
951
952void Fov::setAlgorithm(const std::string &name) {
953 impl_->algorithm = parseAlgorithm(name, impl_->algorithm);
954 impl_->dirty = true;
955}
956
957std::string Fov::getAlgorithm() const { return algorithmName(impl_->algorithm); }
958
959void Fov::setRadiusMetric(const std::string &name) {
960 impl_->metric = parseMetric(name, impl_->metric);
961 impl_->dirty = true;
962}
963
964std::string Fov::getRadiusMetric() const { return metricName(impl_->metric); }
965
966void Fov::setTopology(const std::string &name) {
967 if (name == "auto") {
968 impl_->topologyManual = false;
969 impl_->applyAutoTopologyFromLayer();
970 if (!impl_->layer) impl_->topology = Topology::Ortho;
971 } else if (name == "hex" || name == "hexagonal" || name == "staggered") {
972 impl_->topologyManual = true;
973 impl_->topology = Topology::Hex;
974 } else if (name == "ortho" || name == "orthogonal") {
975 impl_->topologyManual = true;
976 impl_->topology = Topology::Ortho;
977 }
978 impl_->dirty = true;
979}
980
981std::string Fov::getTopology() const { return topologyName(impl_->topology); }
982
983void Fov::setCornerPeek(bool enable) {
984 impl_->cornerPeek = enable;
985 impl_->dirty = true;
986}
987bool Fov::getCornerPeek() const { return impl_->cornerPeek; }
988
989void Fov::blockOpaqueGid(int gid) {
990 if (gid < 0) return;
991 impl_->opaqueGids.insert(uint32_t(gid));
992 if (impl_->layer) impl_->syncFromLayer();
993 else impl_->dirty = true;
994}
995
997 if (gid < 0) return;
998 impl_->opaqueGids.erase(uint32_t(gid));
999 if (impl_->layer) impl_->syncFromLayer();
1000 else impl_->dirty = true;
1001}
1002
1004 impl_->opaqueGids.clear();
1005 if (impl_->layer) impl_->syncFromLayer();
1006 else impl_->dirty = true;
1007}
1008
1009void Fov::setBlockEmpty(bool enable) {
1010 impl_->blockEmpty = enable;
1011 if (impl_->layer) impl_->syncFromLayer();
1012 else impl_->dirty = true;
1013}
1014
1015bool Fov::getBlockEmpty() const { return impl_->blockEmpty; }
1016
1017void Fov::setOpaque(int x, int y, bool opaque) {
1018 if (!impl_->inBounds2(x, y)) return;
1019 if (impl_->mode == Mode::Volume) {
1020 if (!impl_->inBounds3(x, y, 0)) return;
1021 impl_->opaque[size_t(impl_->index3(x, y, 0))] = opaque ? 1u : 0u;
1022 } else {
1023 impl_->opaque[size_t(impl_->index2(x, y))] = opaque ? 1u : 0u;
1024 }
1025 impl_->dirty = true;
1026}
1027
1028bool Fov::isOpaque(int x, int y) const {
1029 if (impl_->mode == Mode::Volume) return impl_->cellOpaque3(x, y, 0);
1030 return impl_->cellOpaque2(x, y);
1031}
1032
1033void Fov::setOpaque3(int x, int y, int z, bool opaque) {
1034 if (!impl_->inBounds3(x, y, z)) return;
1035 impl_->opaque[size_t(impl_->index3(x, y, z))] = opaque ? 1u : 0u;
1036 impl_->dirty = true;
1037}
1038
1039bool Fov::isOpaque3(int x, int y, int z) const { return impl_->cellOpaque3(x, y, z); }
1040
1041void Fov::syncFromLayer() { impl_->syncFromLayer(); }
1042
1043void Fov::setElevation(int x, int y, float elev) {
1044 if (!impl_->inBounds2(x, y)) return;
1045 impl_->elevation[size_t(impl_->index2(x, y))] = elev;
1046 impl_->dirty = true;
1047}
1048
1049float Fov::getElevation(int x, int y) const { return impl_->elevAt(x, y); }
1050
1051void Fov::setCliffBlock(float delta) {
1052 impl_->cliffBlock = delta;
1053 impl_->dirty = true;
1054}
1055float Fov::getCliffBlock() const { return impl_->cliffBlock; }
1056
1058 impl_->eyeOffset = offset;
1059 impl_->dirty = true;
1060}
1061float Fov::getEyeOffset() const { return impl_->eyeOffset; }
1062
1064 impl_->verticalRange = range;
1065 impl_->dirty = true;
1066}
1067int Fov::getVerticalRange() const { return impl_->effectiveVerticalRange(); }
1068
1069int Fov::addRevealer(int x, int y, int radius) { return addRevealer3(x, y, 0, radius); }
1070
1071int Fov::addRevealer3(int x, int y, int z, int radius) {
1073 r.id = impl_->nextRevealerId++;
1074 r.x = x;
1075 r.y = y;
1076 r.z = z;
1077 r.radius = radius;
1078 impl_->revealers.push_back(r);
1079 impl_->dirty = true;
1080 return r.id;
1081}
1082
1084 auto &v = impl_->revealers;
1085 v.erase(std::remove_if(v.begin(), v.end(), [id](const Impl::Revealer &r) { return r.id == id; }),
1086 v.end());
1087 impl_->dirty = true;
1088}
1089
1091 impl_->revealers.clear();
1092 impl_->dirty = true;
1093}
1094
1095void Fov::setRevealerPosition(int id, int x, int y) {
1096 if (auto *r = impl_->findRevealer(id)) setRevealerPosition3(id, x, y, r->z);
1097}
1098
1099void Fov::setRevealerPosition3(int id, int x, int y, int z) {
1100 if (auto *r = impl_->findRevealer(id)) {
1101 r->x = x;
1102 r->y = y;
1103 r->z = z;
1104 impl_->dirty = true;
1105 }
1106}
1107
1109 if (auto *r = impl_->findRevealer(id)) {
1110 r->radius = radius;
1111 impl_->dirty = true;
1112 }
1113}
1114
1115void Fov::setRevealerFacing(int id, float facingDeg, float halfAngleDeg) {
1116 if (auto *r = impl_->findRevealer(id)) {
1117 r->useCone = true;
1118 r->facingDeg = facingDeg;
1119 r->halfAngleDeg = std::max(0.f, halfAngleDeg);
1120 impl_->dirty = true;
1121 }
1122}
1123
1125 if (auto *r = impl_->findRevealer(id)) {
1126 r->useCone = false;
1127 r->halfAngleDeg = 180.f;
1128 impl_->dirty = true;
1129 }
1130}
1131
1133 if (auto *r = impl_->findRevealer(id)) {
1134 r->enabled = enabled;
1135 impl_->dirty = true;
1136 }
1137}
1138
1139int Fov::getRevealerCount() const { return int(impl_->revealers.size()); }
1140
1141void Fov::setRevealerPerception(int id, float perception) {
1142 if (auto *r = impl_->findRevealer(id)) {
1143 r->perception = perception;
1144 impl_->dirty = true;
1145 }
1146}
1147
1148float Fov::getRevealerPerception(int id) const {
1149 if (const auto *r = impl_->findRevealerConst(id)) return r->perception;
1150 return 0.f;
1151}
1152
1154 impl_->perceptionRadiusScale = scale;
1155 impl_->dirty = true;
1156}
1157float Fov::getPerceptionRadiusScale() const { return impl_->perceptionRadiusScale; }
1158
1159void Fov::setDetectionMargin(float margin) { impl_->detectionMargin = margin; }
1160float Fov::getDetectionMargin() const { return impl_->detectionMargin; }
1161
1162int Fov::getEffectiveRadius(int id) const {
1163 if (const auto *r = impl_->findRevealerConst(id)) return impl_->effectiveRadiusOf(*r);
1164 return 0;
1165}
1166
1167bool Fov::canDetect(int revealerId, int x, int y, float targetStealth) const {
1168 if (impl_->mode == Mode::Volume) return canDetect3(revealerId, x, y, 0, targetStealth);
1169 const auto *r = impl_->findRevealerConst(revealerId);
1170 if (!r || !r->enabled) return false;
1171 if (!isVisible(x, y)) return false;
1172 return (r->perception + impl_->detectionMargin) >= targetStealth;
1173}
1174
1175bool Fov::canDetect3(int revealerId, int x, int y, int z, float targetStealth) const {
1176 const auto *r = impl_->findRevealerConst(revealerId);
1177 if (!r || !r->enabled) return false;
1178 if (!isVisible3(x, y, z)) return false;
1179 return (r->perception + impl_->detectionMargin) >= targetStealth;
1180}
1181
1182void Fov::markDirty() { impl_->dirty = true; }
1183bool Fov::isDirty() const { return impl_->dirty; }
1184void Fov::compute() { impl_->compute(); }
1185
1186bool Fov::isVisible(int x, int y) const {
1187 if (impl_->mode == Mode::Volume) return isVisible3(x, y, 0);
1188 if (!impl_->inBounds2(x, y)) return false;
1189 return impl_->state[size_t(impl_->index2(x, y))] == CellState::Visible;
1190}
1191
1192bool Fov::isExplored(int x, int y) const {
1193 if (impl_->mode == Mode::Volume) return isExplored3(x, y, 0);
1194 if (!impl_->inBounds2(x, y)) return false;
1195 const auto s = impl_->state[size_t(impl_->index2(x, y))];
1196 return s == CellState::Explored || s == CellState::Visible;
1197}
1198
1199bool Fov::isVisible3(int x, int y, int z) const {
1200 if (!impl_->inBounds3(x, y, z)) return false;
1201 return impl_->state[size_t(impl_->index3(x, y, z))] == CellState::Visible;
1202}
1203
1204bool Fov::isExplored3(int x, int y, int z) const {
1205 if (!impl_->inBounds3(x, y, z)) return false;
1206 const auto s = impl_->state[size_t(impl_->index3(x, y, z))];
1207 return s == CellState::Explored || s == CellState::Visible;
1208}
1209
1210std::string Fov::getState(int x, int y) const {
1211 if (impl_->mode == Mode::Volume) return getState3(x, y, 0);
1212 if (!impl_->inBounds2(x, y)) return "unknown";
1213 switch (impl_->state[size_t(impl_->index2(x, y))]) {
1214 case CellState::Visible:
1215 return "visible";
1216 case CellState::Explored:
1217 return "explored";
1218 default:
1219 return "unknown";
1220 }
1221}
1222
1223std::string Fov::getState3(int x, int y, int z) const {
1224 if (!impl_->inBounds3(x, y, z)) return "unknown";
1225 switch (impl_->state[size_t(impl_->index3(x, y, z))]) {
1226 case CellState::Visible:
1227 return "visible";
1228 case CellState::Explored:
1229 return "explored";
1230 default:
1231 return "unknown";
1232 }
1233}
1234
1235void Fov::clearMemory() { impl_->clearAllMemory(); }
1236
1238 impl_->resetVisibleKeepExplored();
1239 impl_->dirty = true;
1240}
1241
1243 Snapshot result;
1244 result.width = impl_->width;
1245 result.height = impl_->height;
1246 result.depth = impl_->depth;
1247 result.states.reserve(impl_->state.size());
1248 for (const auto state : impl_->state) result.states.push_back(stateToMaskByte(state));
1249 return result;
1250}
1251
1252Result<void> Fov::restore(const Snapshot& snapshotValue) {
1253 if (snapshotValue.width != impl_->width || snapshotValue.height != impl_->height ||
1254 snapshotValue.depth != impl_->depth || snapshotValue.states.size() != impl_->state.size())
1256 DiagnosticCode::InvalidArgument, "Fov snapshot dimensions or payload size do not match", "snapshot"));
1257 for (std::size_t index = 0; index < snapshotValue.states.size(); ++index) {
1258 const auto value = snapshotValue.states[index];
1259 impl_->state[index] = value == 0 ? CellState::Unknown :
1260 value >= 192 ? CellState::Visible : CellState::Explored;
1261 }
1262 impl_->visibleList.clear();
1263 for (std::size_t index = 0; index < impl_->state.size(); ++index)
1264 if (impl_->state[index] == CellState::Visible) impl_->visibleList.push_back(static_cast<int>(index));
1265 impl_->dirty = true;
1267}
1268
1269float Fov::getMaskValue(int x, int y) const {
1270 if (impl_->mode == Mode::Volume) return getMaskValue3(x, y, 0);
1271 if (!impl_->inBounds2(x, y)) return 0.f;
1272 return stateToMaskValue(impl_->state[size_t(impl_->index2(x, y))]);
1273}
1274
1275int Fov::getMaskByte(int x, int y) const {
1276 if (impl_->mode == Mode::Volume) return getMaskByte3(x, y, 0);
1277 if (!impl_->inBounds2(x, y)) return 0;
1278 return int(stateToMaskByte(impl_->state[size_t(impl_->index2(x, y))]));
1279}
1280
1281float Fov::getMaskValue3(int x, int y, int z) const {
1282 if (!impl_->inBounds3(x, y, z)) return 0.f;
1283 return stateToMaskValue(impl_->state[size_t(impl_->index3(x, y, z))]);
1284}
1285
1286int Fov::getMaskByte3(int x, int y, int z) const {
1287 if (!impl_->inBounds3(x, y, z)) return 0;
1288 return int(stateToMaskByte(impl_->state[size_t(impl_->index3(x, y, z))]));
1289}
1290
1291bool Fov::fillMaskR8(std::vector<uint8_t> &out) const { return fillMaskR8Slice(out, 0); }
1292
1293bool Fov::fillMaskR8Slice(std::vector<uint8_t> &out, int sliceZ) const {
1294 if (impl_->width <= 0 || impl_->height <= 0) {
1295 out.clear();
1296 return false;
1297 }
1298 if (impl_->mode == Mode::Volume) {
1299 if (sliceZ < 0 || sliceZ >= impl_->depth) return false;
1300 } else {
1301 sliceZ = 0;
1302 }
1303 out.resize(size_t(impl_->width * impl_->height));
1304 for (int y = 0; y < impl_->height; ++y) {
1305 for (int x = 0; x < impl_->width; ++x) {
1306 CellState s = (impl_->mode == Mode::Volume)
1307 ? impl_->state[size_t(impl_->index3(x, y, sliceZ))]
1308 : impl_->state[size_t(impl_->index2(x, y))];
1309 out[size_t(impl_->index2(x, y))] = stateToMaskByte(s);
1310 }
1311 }
1312 return true;
1313}
1314
1318
1320 if (!gfx) return nullptr;
1321 std::vector<uint8_t> r8;
1322 if (!fillMaskR8Slice(r8, sliceZ)) return nullptr;
1323 std::vector<uint8_t> rgba(size_t(impl_->width * impl_->height * 4));
1324 for (size_t i = 0; i < r8.size(); ++i) {
1325 const uint8_t v = r8[i];
1326 rgba[i * 4 + 0] = v;
1327 rgba[i * 4 + 1] = v;
1328 rgba[i * 4 + 2] = v;
1329 rgba[i * 4 + 3] = v;
1330 }
1331 return gfx->newTexture(impl_->width, impl_->height, rgba.data());
1332}
1333
1334} // namespace eve::map
double value
Duration start
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int mid
Definition AnimSmr.cpp:120
const std::string & s
float ang
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
float py
float nx
float ny
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
double r
std::int32_t xMin
std::int32_t xMax
float v
std::int32_t c
int h
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
size_t offset
Range range
std::array< float, 3 > scale
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
TileLayer * layer
int idx
float radius
float d
float t
float dz
float dy
float dx
ecs::EntityHandle side
int margin
float step
Definition TreeMesh.cpp:314
V3 dir
Definition TreeMesh.cpp:150
uint32_t index
std::uint32_t depth
double oy
std::vector< char > inside
double ox
float m[16]
std::vector< WfcTile > tiles
Definition WfcSimple.cpp:22
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
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
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
virtual Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false)=0
Creates a texture. @ownership Caller deletes unless documented otherwise.
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
Dynamic field-of-view / fog-of-war facade. Phase A: 2D shadowcast + multi-revealer + explored memory....
Definition Fov.h:27
void blockOpaqueGid(int gid)
Definition Fov.cpp:989
bool canDetect(int revealerId, int x, int y, float targetStealth) const
Definition Fov.cpp:1167
float getElevation(int x, int y) const
Definition Fov.cpp:1049
bool isExplored(int x, int y) const
Definition Fov.cpp:1192
bool isOpaque(int x, int y) const
Definition Fov.cpp:1028
int getVerticalRange() const
Definition Fov.cpp:1067
int getHeight() const
Definition Fov.cpp:938
void bindLayer(TileLayer *layer)
Definition Fov.cpp:920
std::string getRadiusMetric() const
Definition Fov.cpp:964
void setAlgorithm(const std::string &name)
"shadowcast" | "raycast" | "permissive" | "rectangle"
Definition Fov.cpp:952
float getDetectionMargin() const
Definition Fov.cpp:1160
float getEyeOffset() const
Definition Fov.cpp:1061
void setRadiusMetric(const std::string &name)
"euclidean" | "chebyshev" | "manhattan" — ignored when topology is hex (cube distance).
Definition Fov.cpp:959
float getCliffBlock() const
Definition Fov.cpp:1055
void setRevealerEnabled(int id, bool enabled)
Definition Fov.cpp:1132
void setOpaque3(int x, int y, int z, bool opaque)
Definition Fov.cpp:1033
bool isVisible3(int x, int y, int z) const
Definition Fov.cpp:1199
void setDetectionMargin(float margin)
Definition Fov.cpp:1159
void setEyeOffset(float offset)
Definition Fov.cpp:1057
bool isVisible(int x, int y) const
Definition Fov.cpp:1186
int getEffectiveRadius(int id) const
Definition Fov.cpp:1162
void setSize(int width, int height)
Definition Fov.cpp:925
bool fillMaskR8(std::vector< uint8_t > &out) const
Definition Fov.cpp:1291
int getWidth() const
Definition Fov.cpp:937
Result< void > restore(const Snapshot &snapshot)
Restore exact cell states atomically.
Definition Fov.cpp:1252
void setRevealerPosition(int id, int x, int y)
Definition Fov.cpp:1095
float getRevealerPerception(int id) const
Definition Fov.cpp:1148
float getMaskValue3(int x, int y, int z) const
Definition Fov.cpp:1281
int getDepth() const
Definition Fov.cpp:939
void setBlockEmpty(bool enable)
Definition Fov.cpp:1009
std::string getMode() const
Definition Fov.cpp:950
int addRevealer(int x, int y, int radius)
Definition Fov.cpp:1069
void clearMemory()
Definition Fov.cpp:1235
std::string getState3(int x, int y, int z) const
Definition Fov.cpp:1223
float getMaskValue(int x, int y) const
Definition Fov.cpp:1269
void clearOpaqueGids()
Definition Fov.cpp:1003
bool isExplored3(int x, int y, int z) const
Definition Fov.cpp:1204
int getRevealerCount() const
Definition Fov.cpp:1139
graphics::Texture * buildMaskTextureSlice(graphics::Graphics *gfx, int sliceZ) const
Definition Fov.cpp:1319
void setCliffBlock(float delta)
Definition Fov.cpp:1051
void setElevation(int x, int y, float elev)
Definition Fov.cpp:1043
Snapshot snapshot() const
Capture exact unknown/explored/visible cell states without revealers.
Definition Fov.cpp:1242
std::string getTopology() const
Definition Fov.cpp:981
bool fillMaskR8Slice(std::vector< uint8_t > &out, int sliceZ) const
Definition Fov.cpp:1293
int addRevealer3(int x, int y, int z, int radius)
Definition Fov.cpp:1071
std::string getAlgorithm() const
Definition Fov.cpp:957
void setRevealerRadius(int id, int radius)
Definition Fov.cpp:1108
void setRevealerPosition3(int id, int x, int y, int z)
Definition Fov.cpp:1099
void setRevealerPerception(int id, float perception)
Soft RPG hooks (no hard dependency on rpg module). effectiveRadius = radius + floor(perception * perc...
Definition Fov.cpp:1141
void setRevealerFacing(int id, float facingDeg, float halfAngleDeg)
Definition Fov.cpp:1115
void removeRevealer(int id)
Definition Fov.cpp:1083
bool getCornerPeek() const
Definition Fov.cpp:987
void clearRevealers()
Definition Fov.cpp:1090
void clearRevealerFacing(int id)
Definition Fov.cpp:1124
bool isDirty() const
Definition Fov.cpp:1183
void compute()
Definition Fov.cpp:1184
void setPerceptionRadiusScale(float scale)
Definition Fov.cpp:1153
void setMode(const std::string &name)
"grid2d" (default) | "heightmap" | "volume"
Definition Fov.cpp:941
void unblockOpaqueGid(int gid)
Definition Fov.cpp:996
void markDirty()
Definition Fov.cpp:1182
int getMaskByte(int x, int y) const
Definition Fov.cpp:1275
void setVerticalRange(int range)
Definition Fov.cpp:1063
bool canDetect3(int revealerId, int x, int y, int z, float targetStealth) const
Definition Fov.cpp:1175
float getPerceptionRadiusScale() const
Definition Fov.cpp:1157
bool isOpaque3(int x, int y, int z) const
Definition Fov.cpp:1039
bool getBlockEmpty() const
Definition Fov.cpp:1015
std::string getState(int x, int y) const
Definition Fov.cpp:1210
void setCornerPeek(bool enable)
Definition Fov.cpp:983
void setOpaque(int x, int y, bool opaque)
Definition Fov.cpp:1017
graphics::Texture * buildMaskTexture(graphics::Graphics *gfx) const
Upload current 2D / slice mask as RGBA8 Texture (R=G=B=A=mask byte). Caller owns the returned Texture...
Definition Fov.cpp:1315
void resetVisibleOnly()
Definition Fov.cpp:1237
void setTopology(const std::string &name)
"ortho" (default) | "hex" | "auto" (hex if bound layer is hex/staggered).
Definition Fov.cpp:966
void syncFromLayer()
Definition Fov.cpp:1041
void setVolumeSize(int width, int height, int depth)
Definition Fov.cpp:931
int getMaskByte3(int x, int y, int z) const
Definition Fov.cpp:1286
ECS tile layer entity. Script mutates tile GIDs / tileset / draw; TileRenderSystem batch-draws atlas ...
Definition TileLayer.h:32
放置世界:格子占用(多通道)+ 地形语义 + 已放置建筑实例。 行为由 PlacementSystem 提供;本类暴露便于脚本绑定的薄封装方法。 坐标换算统一走 eve::grid(支持 rectang...
uint32_t tileGid(uint32_t raw)
Strip Tiled flip / rotate flags; keep low 28 bits.
Definition TileLayer.h:383
bool enabled
Topology topology
Definition Fov.cpp:227
float hexCellAngle(int ox, int oy, int x, int y) const
Definition Fov.cpp:533
void computeShadowcast(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:611
void computeRaycast(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:660
std::vector< uint8_t > opaque
Definition Fov.cpp:238
void computePermissive(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:694
void computeOrthoShadowcast(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:524
void computeRectangle(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:771
RadiusMetric metric
Definition Fov.cpp:225
const Revealer * findRevealerConst(int id) const
Definition Fov.cpp:389
void markVisibleIdx(int idx)
Definition Fov.cpp:349
std::vector< CellState > state
Definition Fov.cpp:240
void markVisible2(int x, int y)
Definition Fov.cpp:357
int index2(int x, int y) const
Definition Fov.cpp:246
void castRayLine(int ox, int oy, int tx, int ty, int zSlice, bool useCone, float facingDeg, float halfAngleDeg, int radius)
Definition Fov.cpp:616
float detectionMargin
Definition Fov.cpp:236
bool inBounds3(int x, int y, int z) const
Definition Fov.cpp:249
bool passesHeightToTarget(int ox, int oy, int tx, int ty) const
Definition Fov.cpp:473
void syncFromLayer()
Definition Fov.cpp:304
bool cellOpaque3(int x, int y, int z) const
Definition Fov.cpp:334
void markVisible3(int x, int y, int z)
Definition Fov.cpp:362
int effectiveVerticalRange() const
Definition Fov.cpp:253
void resetVisibleKeepExplored()
Definition Fov.cpp:367
std::vector< float > elevation
Definition Fov.cpp:239
float perceptionRadiusScale
Definition Fov.cpp:235
void castLight(int ox, int oy, int radius, int row, float startSlope, float endSlope, int xx, int xy, int yx, int yy, int zSlice, bool useCone, float facingDeg, float halfAngleDeg)
Definition Fov.cpp:488
std::vector< Revealer > revealers
Definition Fov.cpp:243
bool cellOpaque2(int x, int y) const
Definition Fov.cpp:329
std::vector< Rect > buildOpaqueRects(int ox, int oy, int radius, int zSlice) const
Definition Fov.cpp:728
bool inRadius3(int ox, int oy, int oz, int x, int y, int z, int radius) const
Definition Fov.cpp:268
void resize(int w, int h, int d)
Definition Fov.cpp:282
bool angleFullyCovered(float a0, float a1, const std::vector< AngleShadow > &shadows) const
Definition Fov.cpp:541
uint64_t layerRevision
Definition Fov.cpp:223
void computeHexShadowcast(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:569
void applyAutoTopologyFromLayer()
Definition Fov.cpp:275
void addAngleShadow(std::vector< AngleShadow > &shadows, float a0, float a1) const
Definition Fov.cpp:557
void clearAllMemory()
Definition Fov.cpp:376
Revealer * findRevealer(int id)
Definition Fov.cpp:382
std::unordered_set< uint32_t > opaqueGids
Definition Fov.cpp:242
bool los2(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const
Definition Fov.cpp:468
void bindLayer(TileLayer *l)
Definition Fov.cpp:294
bool losBresenham2(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const
Definition Fov.cpp:396
std::vector< int > visibleList
Definition Fov.cpp:241
bool losHexCube(int x0, int y0, int x1, int y1, int zSlice, bool applyHeight) const
Definition Fov.cpp:434
float elevAt(int x, int y) const
Definition Fov.cpp:344
void tryMark2(int ox, int oy, int x, int y, int zSlice, bool useCone, float facingDeg, float halfAngleDeg, int radius)
Definition Fov.cpp:478
int index3(int x, int y, int z) const
Definition Fov.cpp:247
bool inBounds2(int x, int y) const
Definition Fov.cpp:248
int effectiveRadiusOf(const Revealer &r) const
Definition Fov.cpp:258
TileLayer * layer
Definition Fov.cpp:222
bool inRadius2(int ox, int oy, int x, int y, int radius) const
Definition Fov.cpp:263
void computeRevealer(const Revealer &r)
Definition Fov.cpp:875
Algorithm algorithm
Definition Fov.cpp:224
void extendVertical(const Revealer &r, int zSlice, int radius)
Definition Fov.cpp:855
bool cellOpaqueOnSlice(int x, int y, int zSlice) const
Definition Fov.cpp:339
Owning visibility-memory snapshot for the current grid/volume dimensions.
Definition Fov.h:30
std::vector< uint8_t > states
Definition Fov.h:34
Per-GID visual metadata emitted by a project-defined asset pipeline.
Definition TileLayer.h:109