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