1 // _________ __ __
2 // / _____// |_____________ _/ |______ ____ __ __ ______
3 // \_____ \\ __\_ __ \__ \\ __\__ \ / ___\| | \/ ___/
4 // / \| | | | \// __ \| | / __ \_/ /_/ > | /\___ |
5 // /_______ /|__| |__| (____ /__| (____ /\___ /|____//____ >
6 // \/ \/ \//_____/ \/
7 // ______________________ ______________________
8 // T H E W A R B E G I N S
9 // Stratagus - A free fantasy real time strategy game engine
10 //
11 /**@name fov.cpp - The field of view handling. */
12 //
13 // (c) Copyright 2001-2021 by Joris Dauphin, JLutz Sammer,
14 // Vladi Shabanski, Russell Smith, Jimmy Salmon, Pali Rohár, Andrettin
15 // and Alyokhin
16 //
17 // This program is free software; you can redistribute it and/or modify
18 // it under the terms of the GNU General Public License as published by
19 // the Free Software Foundation; only version 2 of the License.
20 //
21 // This program is distributed in the hope that it will be useful,
22 // but WITHOUT ANY WARRANTY; without even the implied warranty of
23 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 // GNU General Public License for more details.
25 //
26 // You should have received a copy of the GNU General Public License
27 // along with this program; if not, write to the Free Software
28 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
29 // 02111-1307, USA.
30 //
31
32 #include <queue>
33 #include "stratagus.h"
34
35 #include "fov.h"
36 #include "settings.h"
37 #include "unit.h"
38 #include "unit_manager.h"
39 #include "unittype.h"
40 #include "util.h"
41
42 /*----------------------------------------------------------------------------
43 -- Variables
44 ----------------------------------------------------------------------------*/
45
46 /*----------------------------------------------------------------------------
47 -- Functions
48 ----------------------------------------------------------------------------*/
49
50 /**
51 ** Select which type of Field of View to use
52 **
53 ** @param fov_type type to set
54 ** @return true if success, false for wrong fov_type
55 */
SetType(const FieldOfViewTypes fov_type)56 bool CFieldOfView::SetType(const FieldOfViewTypes fov_type)
57 {
58 if (fov_type != Settings.Type && fov_type < FieldOfViewTypes::NumOfTypes) {
59 /// Unmark sight for all units
60 MapRefreshUnitsSight(true);
61
62 this->Settings.Type = fov_type;
63
64 /// Mark sight with new fov type for all units
65 MapRefreshUnitsSight();
66 return true;
67 } else {
68 return false;
69 }
70 }
71
72 /**
73 ** Returns used type of Field of View
74 **
75 ** @return current Field of View type
76 */
GetType() const77 FieldOfViewTypes CFieldOfView::GetType() const
78 {
79 return this->Settings.Type;
80 }
81
82 /**
83 ** Set additional opaque map field flags (which terrains will be opaque)
84 **
85 ** @param flags Terrain flags to set as opaque (MapField*)
86 */
SetOpaqueFields(const uint16_t flags)87 void CFieldOfView::SetOpaqueFields(const uint16_t flags)
88 {
89 if (this->Settings.OpaqueFields != flags) {
90 /// Unmark sight for all units
91 MapRefreshUnitsSight(true);
92
93 this->Settings.OpaqueFields = flags;
94
95 /// Mark sight with new fov type for all units
96 MapRefreshUnitsSight();
97 }
98 }
99
100 /**
101 ** Returns current set of opaque map field flags
102 **
103 ** @return Set of terrain flags
104 */
GetOpaqueFields() const105 uint16_t CFieldOfView::GetOpaqueFields() const
106 {
107 return this->Settings.OpaqueFields;
108 }
109
110 /**
111 ** Reset opaque map field flags to default (MapfieldOpaque)
112 **
113 */
ResetAdditionalOpaqueFields()114 void CFieldOfView::ResetAdditionalOpaqueFields()
115 {
116 this->Settings.OpaqueFields = MapFieldOpaque;
117 }
118
119 /**
120 ** Refresh the whole field of view for unit (Explore and make visible.)
121 **
122 ** @param player player to mark the sight for
123 ** @param unit unit to mark the sight for
124 ** @param pos location to mark
125 ** @param width width to mark, in square
126 ** @param height height to mark, in square
127 ** @param range Radius to mark.
128 ** @param marker Function to mark or unmark sight
129 */
Refresh(const CPlayer & player,const CUnit & unit,const Vec2i & pos,const uint16_t width,const uint16_t height,const uint16_t range,MapMarkerFunc * marker)130 void CFieldOfView::Refresh(const CPlayer &player, const CUnit &unit, const Vec2i &pos, const uint16_t width,
131 const uint16_t height, const uint16_t range, MapMarkerFunc *marker)
132 {
133 /// FIXME: sometimes when quit from game this assert is triggered
134 if (!unit.Type) return;
135 Assert(unit.Type != NULL);
136 // Units under construction have no sight range.
137 if (!range) {
138 return;
139 }
140 if (this->Settings.Type == FieldOfViewTypes::cShadowCasting && !unit.Type->AirUnit) {
141 /// FIXME: add high-/lowground
142 OpaqueFields = unit.Type->BoolFlag[ELEVATED_INDEX].value ? 0 : this->Settings.OpaqueFields;
143 if (GameSettings.Inside) {
144 OpaqueFields &= ~(MapFieldRocks); /// because of rocks-flag is used as an obstacle for ranged attackers
145 }
146 PrepareShadowCaster(player, unit, pos, marker);
147 PrepareCache(pos, width, height, range);
148 ProceedShadowCasting(pos, width, height, range + 1);
149 ResetShadowCaster();
150 } else {
151 ProceedSimpleRadial(player, pos, width, height, range, marker);
152 }
153 }
154
155 /**
156 ** Refresh the whole sight of unit by SimleRadial algorithm. (Explore and make visible.)
157 **
158 ** @param player player to mark the sight for (not unit owner)
159 ** @param pos location to mark
160 ** @param w width to mark, in square
161 ** @param h height to mark, in square
162 ** @param range Radius to mark (sight range)
163 ** @param marker Function to mark or unmark sight
164 */
ProceedSimpleRadial(const CPlayer & player,const Vec2i & pos,const int16_t w,const int16_t h,const int16_t range,MapMarkerFunc * marker) const165 void CFieldOfView::ProceedSimpleRadial(const CPlayer &player, const Vec2i &pos,
166 const int16_t w, const int16_t h, const int16_t range,
167 MapMarkerFunc *marker) const
168 {
169 // Up hemi-cyle
170 const int16_t miny = std::max(-range, 0 - pos.y);
171 for (int16_t offsety = miny; offsety != 0; offsety++) {
172 const int16_t offsetx = isqrt(square(range + 1) - square(-offsety) - 1);
173 const int16_t minx = std::max(0, pos.x - offsetx);
174 const int16_t maxx = std::min(Map.Info.MapWidth, pos.x + w + offsetx);
175 Vec2i mpos(minx, pos.y + offsety);
176 const size_t index = mpos.y * Map.Info.MapWidth;
177
178 for (mpos.x = minx; mpos.x < maxx; mpos.x++) {
179 marker(player, mpos.x + index);
180 }
181 }
182 for (int16_t offsety = 0; offsety < h; offsety++) {
183 const int16_t minx = std::max(0, pos.x - range);
184 const int16_t maxx = std::min(Map.Info.MapWidth, pos.x + w + range);
185 Vec2i mpos(minx, pos.y + offsety);
186 const size_t index = mpos.y * Map.Info.MapWidth;
187
188 for (mpos.x = minx; mpos.x < maxx; mpos.x++) {
189 marker(player, mpos.x + index);
190 }
191 }
192 // bottom hemi-cycle
193 const int16_t maxy = std::min(range, int16_t(Map.Info.MapHeight - pos.y - h));
194 for (int16_t offsety = 0; offsety < maxy; offsety++) {
195 const int16_t offsetx = isqrt(square(range + 1) - square(offsety + 1) - 1);
196 const int16_t minx = std::max(0, pos.x - offsetx);
197 const int16_t maxx = std::min(Map.Info.MapWidth, pos.x + w + offsetx);
198 Vec2i mpos(minx, pos.y + h + offsety);
199 const size_t index = mpos.y * Map.Info.MapWidth;
200
201 for (mpos.x = minx; mpos.x < maxx; mpos.x++) {
202 marker(player, mpos.x + index);
203 }
204 }
205 }
206
207 /**
208 ** Mark the sight of unit by ShadowCaster algorithm. (Explore and make visible.)
209 **
210 ** @param spectratorPos Tile position of the spectrator unit - upper left corner for unit larger than 1x1
211 ** @param width Spectrator's width in tiles
212 ** @param height Spectrator's height in tiles
213 ** @param range Spectrator's sight range in tiles
214 */
ProceedShadowCasting(const Vec2i & spectatorPos,const uint16_t width,const uint16_t height,const uint16_t range)215 inline void CFieldOfView::ProceedShadowCasting(const Vec2i &spectatorPos, const uint16_t width, const uint16_t height, const uint16_t range)
216 {
217 enum SpectatorGeometry {cOneTiled, cEven, cOdd, cTall, cWide} ;
218 const uint8_t geometry = [width, height]{ if (width == height) {
219 if (width == 1) { return cOneTiled; }
220 if (width % 2) { return cOdd; }
221 else { return cEven; }
222 }
223 if (width > height) { return cWide; }
224 else { return cTall; }
225 }();
226
227 Vec2i origin = {0, 0};
228 uint16_t sightRange = range;
229
230 const bool isGeometrySymmetric = (geometry == cTall || geometry == cWide) ? false : true;
231
232 if (isGeometrySymmetric) {
233 const int16_t half = width >> 1;
234 origin.x = spectatorPos.x + half;
235 origin.y = spectatorPos.y + half;
236 sightRange += half - (geometry == cEven ? 1 : 0);
237 } else {
238 /// Fill spectator's tiles which not affected by RefreshOctant and ProceedRaysCast
239 ResetEnvironment();
240 for (int16_t x = spectatorPos.x + 1; x < spectatorPos.x + width - 1; x++) {
241 for (int16_t y = spectatorPos.y + 1; y < spectatorPos.y + height - 1; y++) {
242 if (SetCurrentTile(x, y)) {
243 MarkTile();
244 }
245 }
246 }
247 }
248
249 int16_t rayWidth = 0;
250 for (uint8_t quadrant = 0; quadrant < 4; quadrant++) {
251 if (geometry == cEven) {
252 /// recalculate center
253 switch (quadrant) {
254 case 1: origin.x--; break;
255 case 2: origin.y--; break;
256 case 3: origin.x++; break;
257 default: break; /// For quadrant 0 center is altready calculated
258 }
259 } else if (!isGeometrySymmetric) {
260 switch (quadrant) {
261 case 0:
262 origin.x = spectatorPos.x + width - 1;
263 origin.y = spectatorPos.y + height - 1;
264 rayWidth = width - 2;
265 break;
266 case 1:
267 origin.x = spectatorPos.x;
268 rayWidth = height - 2;
269 break;
270 case 2:
271 origin.y = spectatorPos.y;
272 rayWidth = width - 2;
273 break;
274 case 3:
275 origin.x = spectatorPos.x + width - 1;
276 rayWidth = height - 2;
277 break;
278 }
279 }
280 const uint8_t octant = quadrant << 1;
281 /// First half-quadrant
282 RefreshOctant(octant, origin, sightRange);
283 /// Second half-quadrant
284 RefreshOctant(octant + 1, origin, sightRange);
285
286 /// calv FoV for asymmetric spectrator
287 if (rayWidth) {
288 ProceedRaysCast(octant, origin, rayWidth, sightRange);
289 }
290 }
291 }
292
293 /**
294 ** Calc field of view for set of lines along x or y.
295 ** Used for calc part of FoV for assymetric (widht != height) spectators.
296 **
297 ** @param octant Current work octant
298 ** @param origin Tile position of the spectrator
299 ** @param width Spectrator's width in tiles
300 ** @param height Spectrator's height in tiles
301 ** @param range Spectrator's sight range in tiles
302 */
ProceedRaysCast(const uint8_t octant,const Vec2i & origin,const uint16_t width,const uint16_t range)303 void CFieldOfView::ProceedRaysCast(const uint8_t octant, const Vec2i &origin, const uint16_t width, const uint16_t range)
304 {
305 SetEnvironment(octant, origin);
306 for (int16_t col = -1; col >= -width; col--) {
307 for (int16_t row = 0; row < range; row++) {
308 const bool isOnMap = SetCurrentTile(col, row);
309 if (isOnMap) {
310 MarkTile();
311 }
312 if (!isOnMap || IsTileOpaque()) { break; }
313 }
314 }
315 ResetEnvironment();
316 }
317
318 /**
319 ** Calc shadow casting field of view for single octant
320 **
321 ** @param octant Octant to calc for
322 ** @param origin Tile position of the spectrator
323 ** @param range Spectrator's sight range in tiles
324 */
RefreshOctant(const uint8_t octant,const Vec2i & origin,const uint16_t range)325 void CFieldOfView::RefreshOctant(const uint8_t octant, const Vec2i &origin, const uint16_t range)
326 {
327 SetEnvironment(octant, origin);
328 std::queue<SColumnPiece> wrkQueue;
329 wrkQueue.push(SColumnPiece(0, Vec2i(1, 1), Vec2i(1, 0)));
330 while (!wrkQueue.empty()) {
331 SColumnPiece current = wrkQueue.front();
332 wrkQueue.pop();
333 if (current.col >= range) {
334 continue;
335 }
336 CalcFoVForColumnPiece(current.col, current.TopVector, current.BottomVector, range, wrkQueue);
337 }
338 ResetEnvironment();
339 }
340
341 /**
342 ** Calc shadow casting for portion of column
343 **
344 ** @param col Column in current octant
345 ** @param topVector Top direction vector
346 ** @param bottomVector Top direction vector
347 ** @param range Spectrator's sight range in tiles
348 ** @param wrkQueue Queue with all column pieces
349 */
CalcFoVForColumnPiece(const int16_t col,Vec2i & topVector,Vec2i & bottomVector,const uint16_t range,std::queue<SColumnPiece> & wrkQueue)350 void CFieldOfView::CalcFoVForColumnPiece(const int16_t col, Vec2i &topVector, Vec2i &bottomVector,
351 const uint16_t range, std::queue<SColumnPiece> &wrkQueue)
352 {
353 enum { cTop = true, cBottom = false };
354
355 int16_t topRow = CalcRow_ByVector(cTop, col, topVector);
356 int16_t bottomRow = CalcRow_ByVector(cBottom, col, bottomVector);
357
358 enum { cInit, cYes, cNo } wasLastTileOpaque = cInit;
359 for (int16_t row = topRow; row >= bottomRow; --row) {
360 const bool inRange = square(col) + square(row) < square(range);
361 const bool isOnMap = SetCurrentTile(col, row);
362 if (inRange && isOnMap) {
363 MarkTile();
364 }
365 const bool isTileOpaque = !inRange || !isOnMap || IsTileOpaque();
366 if (wasLastTileOpaque != cInit) {
367 if (isTileOpaque) {
368 if (wasLastTileOpaque == cNo) {
369 wrkQueue.push(SColumnPiece(col + 1, topVector, Vec2i(col * 2 - 1, row * 2 + 1)));
370 }
371 } else if (wasLastTileOpaque == cYes) {
372 topVector = {int16_t(col * 2 + 1), int16_t(row * 2 + 1)};
373 }
374 }
375 wasLastTileOpaque = isTileOpaque ? cYes : cNo;
376 }
377 if (wasLastTileOpaque == cNo) {
378 wrkQueue.push(SColumnPiece(col + 1, topVector, bottomVector));
379 }
380 }
381
382 /**
383 ** Recalculate top or bottom direction vector
384 **
385 ** @param isTop Flag to determine Top or Bottom direction vector is proceed
386 ** @param col Current column
387 ** @param vector Current direction vector
388 ** @return Row (Y-value) for new direction vector
389 */
CalcRow_ByVector(const bool isTop,const int16_t col,const Vec2i & vector) const390 int16_t CFieldOfView::CalcRow_ByVector(const bool isTop, const int16_t col, const Vec2i &vector) const
391 {
392 int16_t row;
393 if (col == 0) {
394 row = 0;
395 } else {
396 // (col +|- 0.5) * (top|bot)_vector.y/(top|bot)_vector.x in integers
397 const int16_t devidend = (isTop ? (2 * col + 1) : (2 * col - 1)) * vector.y;
398 const int16_t devisor = 2 * vector.x;
399 const int16_t quotient = devidend / devisor;
400 const int16_t remainder = devidend % devisor;
401 // Round the result
402 if (isTop ? remainder > vector.x
403 : remainder >= vector.x) {
404 row = quotient + 1;
405 } else {
406 row = quotient;
407 }
408 }
409 return row;
410 }
411
PrepareShadowCaster(const CPlayer & player,const CUnit & unit,const Vec2i & pos,MapMarkerFunc * marker)412 void CFieldOfView::PrepareShadowCaster(const CPlayer &player, const CUnit &unit, const Vec2i &pos, MapMarkerFunc *marker)
413 {
414 Player = &player;
415 Unit = &unit;
416 map_setFoV = marker;
417 }
418
ResetShadowCaster()419 void CFieldOfView::ResetShadowCaster()
420 {
421 Player = nullptr;
422 Unit = nullptr;
423 map_setFoV = nullptr;
424 currTilePos = { 0, 0 };
425
426 ResetEnvironment();
427 }
428
PrepareCache(const Vec2i pos,const uint16_t width,const uint16_t height,const uint16_t range)429 void CFieldOfView::PrepareCache(const Vec2i pos, const uint16_t width, const uint16_t height, const uint16_t range)
430 {
431 /// Init cache table if it's uninitialized yet
432 if (!MarkedTilesCache.size()) {
433 MarkedTilesCache.resize(Map.Info.MapWidth * Map.Info.MapWidth);
434 std::fill_n(MarkedTilesCache.begin(), MarkedTilesCache.size(), 0);
435 }
436 /// Clean cache for sight-sized frame
437 Vec2i upperLeft;
438 upperLeft.x = pos.x - range;
439 upperLeft.y = pos.y - range;
440 Map.Clamp(upperLeft);
441
442 Vec2i bottomRight;
443 bottomRight.x = pos.x + width + range;
444 bottomRight.y = pos.y + height + range;
445 Map.Clamp(bottomRight);
446
447 const uint16_t sightRecWidth = bottomRight.x - upperLeft.x + 1;
448
449 size_t index = upperLeft.x + upperLeft.y * Map.Info.MapWidth;
450
451 for (uint16_t y = upperLeft.y; y <= bottomRight.y; y++ ) {
452 std::fill_n(&MarkedTilesCache[index], sightRecWidth, 0);
453 index += Map.Info.MapWidth;
454 }
455 }
456
457 /**
458 ** Update values of Octant and Origin for current working set
459 **
460 ** @param octant Octant to calc for
461 ** @param origin Tile position of the spectrator
462 */
SetEnvironment(const uint8_t octant,const Vec2i & origin)463 void CFieldOfView::SetEnvironment(const uint8_t octant, const Vec2i &origin)
464 {
465 Origin = origin;
466 currOctant = octant;
467 }
468
ResetEnvironment()469 void CFieldOfView::ResetEnvironment()
470 {
471 Origin = { 0, 0 };
472 currOctant = 0;
473 }
474
475 //@}
476