/**
* @ file lighting . cpp
*
* Implementation of light and vision .
*/
# include "lighting.h"
# include <algorithm>
# include <cstdint>
# include <numeric>
# include <string>
# include <expected.hpp>
# include "automap.h"
# include "engine/load_file.hpp"
# include "engine/points_in_rectangle_range.hpp"
# include "player.h"
# include "utils/attributes.h"
# include "utils/is_of.hpp"
# include "utils/status_macros.hpp"
namespace devilution {
std : : array < bool , MAXVISION > VisionActive ;
Light VisionList [ MAXVISION ] ;
Light Lights [ MAXLIGHTS ] ;
std : : array < uint8_t , MAXLIGHTS > ActiveLights ;
int ActiveLightCount ;
std : : array < std : : array < uint8_t , 256 > , NumLightingLevels > LightTables ;
uint8_t * FullyLitLightTable = nullptr ;
uint8_t * FullyDarkLightTable = nullptr ;
std : : array < uint8_t , 256 > InfravisionTable ;
std : : array < uint8_t , 256 > StoneTable ;
std : : array < uint8_t , 256 > PauseTable ;
# ifdef _DEBUG
bool DisableLighting ;
# endif
bool UpdateLighting ;
namespace {
/*
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
* XY points of vision rays are cast to trace the visibility of the
* surrounding environment . The table represents N rays of M points in
* one quadrant ( 0 ° - 90 ° ) of a circle , so rays for other quadrants will
* be created by mirroring . Zero points at the end will be trimmed and
* ignored . A similar table can be recreated using Bresenham ' s line
* drawing algorithm , which is suitable for integer arithmetic :
* https : //en.wikipedia.org/wiki/Bresenham's_line_algorithm
*/
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
static const DisplacementOf < int8_t > VisionRays [ 23 ] [ 15 ] = {
// clang-format off
{ { 1 , 0 } , { 2 , 0 } , { 3 , 0 } , { 4 , 0 } , { 5 , 0 } , { 6 , 0 } , { 7 , 0 } , { 8 , 0 } , { 9 , 0 } , { 10 , 0 } , { 11 , 0 } , { 12 , 0 } , { 13 , 0 } , { 14 , 0 } , { 15 , 0 } } ,
{ { 1 , 0 } , { 2 , 0 } , { 3 , 0 } , { 4 , 0 } , { 5 , 0 } , { 6 , 0 } , { 7 , 0 } , { 8 , 1 } , { 9 , 1 } , { 10 , 1 } , { 11 , 1 } , { 12 , 1 } , { 13 , 1 } , { 14 , 1 } , { 15 , 1 } } ,
{ { 1 , 0 } , { 2 , 0 } , { 3 , 0 } , { 4 , 1 } , { 5 , 1 } , { 6 , 1 } , { 7 , 1 } , { 8 , 1 } , { 9 , 1 } , { 10 , 1 } , { 11 , 1 } , { 12 , 2 } , { 13 , 2 } , { 14 , 2 } , { 15 , 2 } } ,
{ { 1 , 0 } , { 2 , 0 } , { 3 , 1 } , { 4 , 1 } , { 5 , 1 } , { 6 , 1 } , { 7 , 1 } , { 8 , 2 } , { 9 , 2 } , { 10 , 2 } , { 11 , 2 } , { 12 , 2 } , { 13 , 3 } , { 14 , 3 } , { 15 , 3 } } ,
{ { 1 , 0 } , { 2 , 1 } , { 3 , 1 } , { 4 , 1 } , { 5 , 1 } , { 6 , 2 } , { 7 , 2 } , { 8 , 2 } , { 9 , 3 } , { 10 , 3 } , { 11 , 3 } , { 12 , 3 } , { 13 , 4 } , { 14 , 4 } , { 0 , 0 } } ,
{ { 1 , 0 } , { 2 , 1 } , { 3 , 1 } , { 4 , 1 } , { 5 , 2 } , { 6 , 2 } , { 7 , 3 } , { 8 , 3 } , { 9 , 3 } , { 10 , 4 } , { 11 , 4 } , { 12 , 4 } , { 13 , 5 } , { 14 , 5 } , { 0 , 0 } } ,
{ { 1 , 0 } , { 2 , 1 } , { 3 , 1 } , { 4 , 2 } , { 5 , 2 } , { 6 , 3 } , { 7 , 3 } , { 8 , 3 } , { 9 , 4 } , { 10 , 4 } , { 11 , 5 } , { 12 , 5 } , { 13 , 6 } , { 14 , 6 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 1 } , { 3 , 2 } , { 4 , 2 } , { 5 , 3 } , { 6 , 3 } , { 7 , 4 } , { 8 , 4 } , { 9 , 5 } , { 10 , 5 } , { 11 , 6 } , { 12 , 6 } , { 13 , 7 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 1 } , { 3 , 2 } , { 4 , 2 } , { 5 , 3 } , { 6 , 4 } , { 7 , 4 } , { 8 , 5 } , { 9 , 6 } , { 10 , 6 } , { 11 , 7 } , { 12 , 7 } , { 12 , 8 } , { 13 , 8 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 2 } , { 3 , 2 } , { 4 , 3 } , { 5 , 4 } , { 6 , 5 } , { 7 , 5 } , { 8 , 6 } , { 9 , 7 } , { 10 , 7 } , { 10 , 8 } , { 11 , 8 } , { 12 , 9 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 2 } , { 3 , 3 } , { 4 , 4 } , { 5 , 5 } , { 6 , 5 } , { 7 , 6 } , { 8 , 7 } , { 9 , 8 } , { 10 , 9 } , { 11 , 9 } , { 11 , 10 } , { 0 , 0 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 2 } , { 3 , 3 } , { 4 , 4 } , { 5 , 5 } , { 6 , 6 } , { 7 , 7 } , { 8 , 8 } , { 9 , 9 } , { 10 , 10 } , { 11 , 11 } , { 0 , 0 } , { 0 , 0 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 2 } , { 3 , 3 } , { 4 , 4 } , { 5 , 5 } , { 5 , 6 } , { 6 , 7 } , { 7 , 8 } , { 8 , 9 } , { 9 , 10 } , { 9 , 11 } , { 10 , 11 } , { 0 , 0 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 2 , 2 } , { 2 , 3 } , { 3 , 4 } , { 4 , 5 } , { 5 , 6 } , { 5 , 7 } , { 6 , 8 } , { 7 , 9 } , { 7 , 10 } , { 8 , 10 } , { 8 , 11 } , { 9 , 12 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 1 , 2 } , { 2 , 3 } , { 2 , 4 } , { 3 , 5 } , { 4 , 6 } , { 4 , 7 } , { 5 , 8 } , { 6 , 9 } , { 6 , 10 } , { 7 , 11 } , { 7 , 12 } , { 8 , 12 } , { 8 , 13 } , { 0 , 0 } } ,
{ { 1 , 1 } , { 1 , 2 } , { 2 , 3 } , { 2 , 4 } , { 3 , 5 } , { 3 , 6 } , { 4 , 7 } , { 4 , 8 } , { 5 , 9 } , { 5 , 10 } , { 6 , 11 } , { 6 , 12 } , { 7 , 13 } , { 0 , 0 } , { 0 , 0 } } ,
{ { 0 , 1 } , { 1 , 2 } , { 1 , 3 } , { 2 , 4 } , { 2 , 5 } , { 3 , 6 } , { 3 , 7 } , { 3 , 8 } , { 4 , 9 } , { 4 , 10 } , { 5 , 11 } , { 5 , 12 } , { 6 , 13 } , { 6 , 14 } , { 0 , 0 } } ,
{ { 0 , 1 } , { 1 , 2 } , { 1 , 3 } , { 1 , 4 } , { 2 , 5 } , { 2 , 6 } , { 3 , 7 } , { 3 , 8 } , { 3 , 9 } , { 4 , 10 } , { 4 , 11 } , { 4 , 12 } , { 5 , 13 } , { 5 , 14 } , { 0 , 0 } } ,
{ { 0 , 1 } , { 1 , 2 } , { 1 , 3 } , { 1 , 4 } , { 1 , 5 } , { 2 , 6 } , { 2 , 7 } , { 2 , 8 } , { 3 , 9 } , { 3 , 10 } , { 3 , 11 } , { 3 , 12 } , { 4 , 13 } , { 4 , 14 } , { 0 , 0 } } ,
{ { 0 , 1 } , { 0 , 2 } , { 1 , 3 } , { 1 , 4 } , { 1 , 5 } , { 1 , 6 } , { 1 , 7 } , { 2 , 8 } , { 2 , 9 } , { 2 , 10 } , { 2 , 11 } , { 2 , 12 } , { 3 , 13 } , { 3 , 14 } , { 3 , 15 } } ,
{ { 0 , 1 } , { 0 , 2 } , { 0 , 3 } , { 1 , 4 } , { 1 , 5 } , { 1 , 6 } , { 1 , 7 } , { 1 , 8 } , { 1 , 9 } , { 1 , 10 } , { 1 , 11 } , { 2 , 12 } , { 2 , 13 } , { 2 , 14 } , { 2 , 15 } } ,
{ { 0 , 1 } , { 0 , 2 } , { 0 , 3 } , { 0 , 4 } , { 0 , 5 } , { 0 , 6 } , { 0 , 7 } , { 1 , 8 } , { 1 , 9 } , { 1 , 10 } , { 1 , 11 } , { 1 , 12 } , { 1 , 13 } , { 1 , 14 } , { 1 , 15 } } ,
{ { 0 , 1 } , { 0 , 2 } , { 0 , 3 } , { 0 , 4 } , { 0 , 5 } , { 0 , 6 } , { 0 , 7 } , { 0 , 8 } , { 0 , 9 } , { 0 , 10 } , { 0 , 11 } , { 0 , 12 } , { 0 , 13 } , { 0 , 14 } , { 0 , 15 } } ,
// clang-format on
} ;
/** @brief Number of supported light radiuses (first radius starts with 0) */
constexpr size_t NumLightRadiuses = 16 ;
/** Falloff tables for the light cone */
uint8_t LightFalloffs [ NumLightRadiuses ] [ 128 ] ;
bool UpdateVision ;
/** interpolations of a 32x32 (16x16 mirrored) light circle moving between tiles in steps of 1/8 of a tile */
uint8_t LightConeInterpolations [ 8 ] [ 8 ] [ 16 ] [ 16 ] ;
void RotateRadius ( DisplacementOf < int8_t > & offset , DisplacementOf < int8_t > & dist , DisplacementOf < int8_t > & light , DisplacementOf < int8_t > & block )
{
dist = { static_cast < int8_t > ( 7 - dist . deltaY ) , dist . deltaX } ;
light = { static_cast < int8_t > ( 7 - light . deltaY ) , light . deltaX } ;
offset = { static_cast < int8_t > ( dist . deltaX - light . deltaX ) , static_cast < int8_t > ( dist . deltaY - light . deltaY ) } ;
block . deltaX = 0 ;
if ( offset . deltaX < 0 ) {
offset . deltaX + = 8 ;
block . deltaX = 1 ;
}
block . deltaY = 0 ;
if ( offset . deltaY < 0 ) {
offset . deltaY + = 8 ;
block . deltaY = 1 ;
}
}
DVL_ALWAYS_INLINE void SetLight ( Point position , uint8_t v )
{
if ( LoadingMapObjects )
dPreLight [ position . x ] [ position . y ] = v ;
else
dLight [ position . x ] [ position . y ] = v ;
}
DVL_ALWAYS_INLINE uint8_t GetLight ( Point position )
{
if ( LoadingMapObjects )
return dPreLight [ position . x ] [ position . y ] ;
return dLight [ position . x ] [ position . y ] ;
}
bool TileAllowsLight ( Point position )
{
if ( ! InDungeonBounds ( position ) )
return false ;
return ! TileHasAny ( position , TileProperties : : BlockLight ) ;
}
void DoVisionFlags ( Point position , MapExplorationType doAutomap , bool visible )
{
if ( doAutomap ! = MAP_EXP_NONE ) {
if ( dFlags [ position . x ] [ position . y ] ! = DungeonFlag : : None )
SetAutomapView ( position , doAutomap ) ;
dFlags [ position . x ] [ position . y ] | = DungeonFlag : : Explored ;
}
if ( visible )
dFlags [ position . x ] [ position . y ] | = DungeonFlag : : Lit ;
dFlags [ position . x ] [ position . y ] | = DungeonFlag : : Visible ;
}
} // namespace
void DoUnLight ( Point position , uint8_t radius )
{
radius + + ;
radius + + ; // If lights moved at a diagonal it can result in some extra tiles being lit
auto searchArea = PointsInRectangle ( WorldTileRectangle { position , radius } ) ;
for ( WorldTilePosition targetPosition : searchArea ) {
if ( InDungeonBounds ( targetPosition ) )
dLight [ targetPosition . x ] [ targetPosition . y ] = dPreLight [ targetPosition . x ] [ targetPosition . y ] ;
}
}
void DoLighting ( Point position , uint8_t radius , DisplacementOf < int8_t > offset )
{
assert ( radius > = 0 & & radius < = NumLightRadiuses ) ;
assert ( InDungeonBounds ( position ) ) ;
DisplacementOf < int8_t > light = { } ;
DisplacementOf < int8_t > block = { } ;
if ( offset . deltaX < 0 ) {
offset . deltaX + = 8 ;
position - = { 1 , 0 } ;
}
if ( offset . deltaY < 0 ) {
offset . deltaY + = 8 ;
position - = { 0 , 1 } ;
}
DisplacementOf < int8_t > dist = offset ;
int minX = 15 ;
if ( position . x - 15 < 0 ) {
minX = position . x + 1 ;
}
int maxX = 15 ;
if ( position . x + 15 > MAXDUNX ) {
maxX = MAXDUNX - position . x ;
}
int minY = 15 ;
if ( position . y - 15 < 0 ) {
minY = position . y + 1 ;
}
int maxY = 15 ;
if ( position . y + 15 > MAXDUNY ) {
maxY = MAXDUNY - position . y ;
}
// Allow for dim lights in crypt and nest
if ( IsAnyOf ( leveltype , DTYPE_NEST , DTYPE_CRYPT ) ) {
if ( GetLight ( position ) > LightFalloffs [ radius ] [ 0 ] )
SetLight ( position , LightFalloffs [ radius ] [ 0 ] ) ;
} else {
SetLight ( position , 0 ) ;
}
for ( int i = 0 ; i < 4 ; i + + ) {
int yBound = i > 0 & & i < 3 ? maxY : minY ;
int xBound = i < 2 ? maxX : minX ;
for ( int y = 0 ; y < yBound ; y + + ) {
for ( int x = 1 ; x < xBound ; x + + ) {
int linearDistance = LightConeInterpolations [ offset . deltaX ] [ offset . deltaY ] [ x + block . deltaX ] [ y + block . deltaY ] ;
if ( linearDistance > = 128 )
continue ;
Point temp = position + ( Displacement { x , y } ) . Rotate ( - i ) ;
uint8_t v = LightFalloffs [ radius ] [ linearDistance ] ;
if ( ! InDungeonBounds ( temp ) )
continue ;
if ( v < GetLight ( temp ) )
SetLight ( temp , v ) ;
}
}
RotateRadius ( offset , dist , light , block ) ;
}
}
void DoUnVision ( Point position , uint8_t radius )
{
radius + + ;
radius + + ; // increasing the radius even further here prevents leaving stray vision tiles behind and doesn't seem to affect monster AI - applying new vision happens in the same tick
auto searchArea = PointsInRectangle ( WorldTileRectangle { position , radius } ) ;
for ( WorldTilePosition targetPosition : searchArea ) {
if ( InDungeonBounds ( targetPosition ) )
dFlags [ targetPosition . x ] [ targetPosition . y ] & = ~ ( DungeonFlag : : Visible | DungeonFlag : : Lit ) ;
}
}
void DoVision ( Point position , uint8_t radius , MapExplorationType doAutomap , bool visible )
{
DoVisionFlags ( position , doAutomap , visible ) ;
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
// Adjustment to a ray length to ensure all rays lie on an
// accurate circle
static const uint8_t rayLenAdj [ 23 ] = { 0 , 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 3 , 4 , 3 , 2 , 2 , 2 , 1 , 1 , 1 , 0 , 0 , 0 , 0 } ;
static_assert ( std : : size ( rayLenAdj ) = = std : : size ( VisionRays ) ) ;
// Four quadrants on a circle
static const Displacement quadrants [ ] = { { 1 , 1 } , { - 1 , 1 } , { 1 , - 1 } , { - 1 , - 1 } } ;
// Loop over quadrants and mirror rays for each one
for ( const auto & quadrant : quadrants ) {
// Cast a ray for a quadrant
for ( unsigned int j = 0 ; j < std : : size ( VisionRays ) ; j + + ) {
int rayLen = radius - rayLenAdj [ j ] ;
for ( int k = 0 ; k < rayLen ; k + + ) {
const auto & relRayPoint = VisionRays [ j ] [ k ] ;
// Calculate the next point on a ray in the quadrant
Point rayPoint = position + relRayPoint * quadrant ;
if ( ! InDungeonBounds ( rayPoint ) )
break ;
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
// We've cast an approximated ray on an integer 2D
// grid, so we need to check if a ray can pass through
// the diagonally adjacent tiles. For example, consider
// this case:
//
// #?
// ↗ #
// x
//
// The ray is cast from the observer 'x', and reaches
// the '?', but diagonally adjacent tiles '#' do not
// pass the light, so the '?' should not be visible
// for the 2D observer.
//
// The trick is to perform two additional visibility
// checks for the diagonally adjacent tiles, but only
// for the rays that are not parallel to the X or Y
// coordinate lines. Parallel rays, which have a 0 in
// one of their coordinate components, do not require
// any additional adjacent visibility checks, and the
// tile, hit by the ray, is always considered visible.
//
if ( relRayPoint . deltaX > 0 & & relRayPoint . deltaY > 0 ) {
Displacement adjacent1 = { - quadrant . deltaX , 0 } ;
Displacement adjacent2 = { 0 , - quadrant . deltaY } ;
bool passesLight = ( TileAllowsLight ( rayPoint + adjacent1 ) | | TileAllowsLight ( rayPoint + adjacent2 ) ) ;
if ( ! passesLight )
// Diagonally adjacent tiles do not pass the
// light further, we are done with this ray
break ;
}
DoVisionFlags ( rayPoint , doAutomap , visible ) ;
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
bool passesLight = TileAllowsLight ( rayPoint ) ;
if ( ! passesLight )
// Tile does not pass the light further, we are
// done with this ray
break ;
lighting: fix long-standing issue with invisible objects (#7901)
* lighting: fix long-standing issue with invisible objects
This fixes an issue with lighting, where objects in a straight line of
sight parallel to the X or Y coordinate lines become invisible. Issue
#6641 perfectly illustrates the bug (see video attached to the bug).
What's worse, the objects are invisible to the observer (player)
regardless of the distance to that objects. The main requirement of a
bug reproduction is line of sight parallel to the X or Y coordinate
lines.
The actual bug lies in the visibility checks of adjacent tiles of a
point, hit by the cast ray. We've cast an approximated ray on an
integer 2D grid, so we need to check if a ray can pass through the
diagonally adjacent tiles. For example, consider this case:
#?
↗ #
x
The ray is cast from the observer 'x', and reaches the '?', but
diagonally adjacent tiles '#' do not pass the light, so the '?' should
not be visible for the 2D observer.
The trick is to perform two additional visibility checks for the
diagonally adjacent tiles, but only for the rays that are not parallel
to the X or Y coordinate lines. Parallel rays, which have a 0 in one
of their coordinate components, do not require any additional adjacent
visibility checks, and the tile, hit by the ray, is always considered
visible.
For the rays that parallel to the X or Y coordinate lines, the adjacent
visibility check always degenerated to the actual ray point visibility
check, which is considered invisible if it does not allow light to
pass through, and this is the actual bug.
To fix the issue, ensure the tile is always considered visible if the
ray that hits it is parallel to the X or Y coordinate lines.
To better demonstrate the problem, here's a straightforward simulation
written in Python:
https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5
The code simulates lighting from the DevilitionX implementation, by
placing the observer 'x' in the center of the grid. The observer is
surrounded by walls and 5 random obstacles, '.' are marked as visible,
were hit by the cast rays. The first matrix output shows the bug: no
walls and obstacles are visible in the line of sight parallel to the X
and Y coordinate lines. In contrast, the second matrix output (with
the fix applied) does not exhibit this problem. Also, note the box
corners are not visible due to the adjacent visibility checks, which
are functioning correctly.
Fixes: #6641
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* lighting: rename variables and add explicit comments for clarity
This patch improves clarity and readability without affecting
functionality:
1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to
`rayPoint` for better clarity on the purpose of these structures.
2. Renames `factors` to `quadrants` to reflect the actual purpose of
the mirror operation along the X or Y coordinate lines.
3. Adds more explicit comments to simplify the understanding of the
ray casting algorithm.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
* test/WarriorLevel1to2: update timedemo
Recent visibility fix impacts game state and causes the timedemo to
behave completely differently, resulting in a butterfly effect:
https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which
was recorded with the visibility fixes applied, ensuring the tests
pass successfully. Here's the latest timedemo video for the future
generations: https://youtu.be/udGcWmarYNI
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
---------
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
int8_t trans = dTransVal [ rayPoint . x ] [ rayPoint . y ] ;
if ( trans ! = 0 )
TransList [ trans ] = true ;
}
}
}
}
tl : : expected < void , std : : string > LoadTrns ( )
{
RETURN_IF_ERROR ( LoadFileInMemWithStatus ( " plrgfx \\ infra.trn " , InfravisionTable ) ) ;
RETURN_IF_ERROR ( LoadFileInMemWithStatus ( " plrgfx \\ stone.trn " , StoneTable ) ) ;
return LoadFileInMemWithStatus ( " gendata \\ pause.trn " , PauseTable ) ;
}
void MakeLightTable ( )
{
// Generate 16 gradually darker translation tables for doing lighting
uint8_t shade = 0 ;
constexpr uint8_t Black = 0 ;
constexpr uint8_t White = 255 ;
for ( auto & lightTable : LightTables ) {
uint8_t colorIndex = 0 ;
for ( uint8_t steps : { 16 , 16 , 16 , 16 , 16 , 16 , 16 , 16 , 8 , 8 , 8 , 8 , 16 , 16 , 16 , 16 , 16 , 16 } ) {
const uint8_t shading = shade * steps / 16 ;
const uint8_t shadeStart = colorIndex ;
const uint8_t shadeEnd = shadeStart + steps - 1 ;
for ( uint8_t step = 0 ; step < steps ; step + + ) {
if ( colorIndex = = Black ) {
lightTable [ colorIndex + + ] = Black ;
continue ;
}
int color = shadeStart + step + shading ;
if ( color > shadeEnd | | color = = White )
color = Black ;
lightTable [ colorIndex + + ] = color ;
}
}
shade + + ;
}
LightTables [ 15 ] = { } ; // Make last shade pitch black
FullyLitLightTable = LightTables [ 0 ] . data ( ) ;
FullyDarkLightTable = LightTables [ LightsMax ] . data ( ) ;
if ( leveltype = = DTYPE_HELL ) {
// Blood wall lighting
const auto shades = static_cast < int > ( LightTables . size ( ) - 1 ) ;
for ( int i = 0 ; i < shades ; i + + ) {
auto & lightTable = LightTables [ i ] ;
constexpr int Range = 16 ;
for ( int j = 0 ; j < Range ; j + + ) {
uint8_t color = ( ( Range - 1 ) < < 4 ) / shades * ( shades - i ) / Range * ( j + 1 ) ;
color = 1 + ( color > > 4 ) ;
int idx = j + 1 ;
lightTable [ idx ] = color ;
idx = 31 - j ;
lightTable [ idx ] = color ;
}
}
FullyLitLightTable = nullptr ; // A color map is used for the ceiling animation, so even fully lit tiles have a color map
} else if ( IsAnyOf ( leveltype , DTYPE_NEST , DTYPE_CRYPT ) ) {
// Make the lava fully bright
for ( auto & lightTable : LightTables )
std : : iota ( lightTable . begin ( ) , lightTable . begin ( ) + 16 , uint8_t { 0 } ) ;
LightTables [ 15 ] [ 0 ] = 0 ;
std : : fill_n ( LightTables [ 15 ] . begin ( ) + 1 , 15 , 1 ) ;
FullyDarkLightTable = nullptr ; // Tiles in Hellfire levels are never completely black
}
// Verify that fully lit and fully dark light table optimizations are correctly enabled/disabled (nullptr = disabled)
assert ( ( FullyLitLightTable ! = nullptr ) = = ( LightTables [ 0 ] [ 0 ] = = 0 & & std : : adjacent_find ( LightTables [ 0 ] . begin ( ) , LightTables [ 0 ] . end ( ) - 1 , [ ] ( auto x , auto y ) { return ( x + 1 ) ! = y ; } ) = = LightTables [ 0 ] . end ( ) - 1 ) ) ;
assert ( ( FullyDarkLightTable ! = nullptr ) = = ( std : : all_of ( LightTables [ LightsMax ] . begin ( ) , LightTables [ LightsMax ] . end ( ) , [ ] ( auto x ) { return x = = 0 ; } ) ) ) ;
// Generate light falloffs ranges
const float maxDarkness = 15 ;
const float maxBrightness = 0 ;
for ( unsigned radius = 0 ; radius < NumLightRadiuses ; radius + + ) {
const unsigned maxDistance = ( radius + 1 ) * 8 ;
for ( unsigned distance = 0 ; distance < 128 ; distance + + ) {
if ( distance > maxDistance ) {
LightFalloffs [ radius ] [ distance ] = 15 ;
} else {
const float factor = static_cast < float > ( distance ) / static_cast < float > ( maxDistance ) ;
float scaled ;
if ( IsAnyOf ( leveltype , DTYPE_NEST , DTYPE_CRYPT ) ) {
// quardratic falloff with over exposure
const float brightness = static_cast < float > ( radius ) * 1.25F ;
scaled = factor * factor * brightness + ( maxDarkness - brightness ) ;
scaled = std : : max ( maxBrightness , scaled ) ;
} else {
// Leaner falloff
scaled = factor * maxDarkness ;
}
scaled + = 0.5F ; // Round up
LightFalloffs [ radius ] [ distance ] = static_cast < uint8_t > ( scaled ) ;
}
}
}
// Generate the light cone interpolations
for ( int offsetY = 0 ; offsetY < 8 ; offsetY + + ) {
for ( int offsetX = 0 ; offsetX < 8 ; offsetX + + ) {
for ( int y = 0 ; y < 16 ; y + + ) {
for ( int x = 0 ; x < 16 ; x + + ) {
int a = ( 8 * x - offsetX ) ;
int b = ( 8 * y - offsetY ) ;
LightConeInterpolations [ offsetX ] [ offsetY ] [ x ] [ y ] = static_cast < uint8_t > ( sqrt ( a * a + b * b ) ) ;
}
}
}
}
}
# ifdef _DEBUG
void ToggleLighting ( )
{
DisableLighting = ! DisableLighting ;
if ( DisableLighting ) {
memset ( dLight , 0 , sizeof ( dLight ) ) ;
return ;
}
memcpy ( dLight , dPreLight , sizeof ( dLight ) ) ;
for ( const Player & player : Players ) {
if ( player . plractive & & player . isOnActiveLevel ( ) ) {
DoLighting ( player . position . tile , player . _pLightRad , { } ) ;
}
}
}
# endif
void InitLighting ( )
{
ActiveLightCount = 0 ;
UpdateLighting = false ;
UpdateVision = false ;
# ifdef _DEBUG
DisableLighting = false ;
# endif
std : : iota ( ActiveLights . begin ( ) , ActiveLights . end ( ) , uint8_t { 0 } ) ;
VisionActive = { } ;
TransList = { } ;
}
int AddLight ( Point position , uint8_t radius )
{
# ifdef _DEBUG
if ( DisableLighting )
return NO_LIGHT ;
# endif
if ( ActiveLightCount > = MAXLIGHTS )
return NO_LIGHT ;
int lid = ActiveLights [ ActiveLightCount + + ] ;
Light & light = Lights [ lid ] ;
light . position . tile = position ;
light . radius = radius ;
light . position . offset = { 0 , 0 } ;
light . isInvalid = false ;
light . hasChanged = false ;
UpdateLighting = true ;
return lid ;
}
void AddUnLight ( int i )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( i = = NO_LIGHT )
return ;
Lights [ i ] . isInvalid = true ;
UpdateLighting = true ;
}
void ChangeLightRadius ( int i , uint8_t radius )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( i = = NO_LIGHT )
return ;
Light & light = Lights [ i ] ;
light . hasChanged = true ;
light . position . old = light . position . tile ;
light . oldRadius = light . radius ;
light . radius = radius ;
UpdateLighting = true ;
}
void ChangeLightXY ( int i , Point position )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( i = = NO_LIGHT )
return ;
Light & light = Lights [ i ] ;
light . hasChanged = true ;
light . position . old = light . position . tile ;
light . oldRadius = light . radius ;
light . position . tile = position ;
UpdateLighting = true ;
}
void ChangeLightOffset ( int i , DisplacementOf < int8_t > offset )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( i = = NO_LIGHT )
return ;
Light & light = Lights [ i ] ;
if ( light . position . offset = = offset )
return ;
light . hasChanged = true ;
light . position . old = light . position . tile ;
light . oldRadius = light . radius ;
light . position . offset = offset ;
UpdateLighting = true ;
}
void ChangeLight ( int i , Point position , uint8_t radius )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( i = = NO_LIGHT )
return ;
Light & light = Lights [ i ] ;
light . hasChanged = true ;
light . position . old = light . position . tile ;
light . oldRadius = light . radius ;
light . position . tile = position ;
light . radius = radius ;
UpdateLighting = true ;
}
void ProcessLightList ( )
{
# ifdef _DEBUG
if ( DisableLighting )
return ;
# endif
if ( ! UpdateLighting )
return ;
for ( int i = 0 ; i < ActiveLightCount ; i + + ) {
Light & light = Lights [ ActiveLights [ i ] ] ;
if ( light . isInvalid ) {
DoUnLight ( light . position . tile , light . radius ) ;
}
if ( light . hasChanged ) {
DoUnLight ( light . position . old , light . oldRadius ) ;
light . hasChanged = false ;
}
}
for ( int i = 0 ; i < ActiveLightCount ; i + + ) {
const Light & light = Lights [ ActiveLights [ i ] ] ;
if ( light . isInvalid ) {
ActiveLightCount - - ;
std : : swap ( ActiveLights [ ActiveLightCount ] , ActiveLights [ i ] ) ;
i - - ;
continue ;
}
if ( TileHasAny ( light . position . tile , TileProperties : : Solid ) )
continue ; // Monster hidden in a wall, don't spoil the surprise
DoLighting ( light . position . tile , light . radius , light . position . offset ) ;
}
UpdateLighting = false ;
}
void SavePreLighting ( )
{
memcpy ( dPreLight , dLight , sizeof ( dPreLight ) ) ;
}
void ActivateVision ( Point position , int r , size_t id )
{
auto & vision = VisionList [ id ] ;
vision . position . tile = position ;
vision . radius = r ;
vision . isInvalid = false ;
vision . hasChanged = false ;
VisionActive [ id ] = true ;
UpdateVision = true ;
}
void ChangeVisionRadius ( size_t id , int r )
{
auto & vision = VisionList [ id ] ;
vision . hasChanged = true ;
vision . position . old = vision . position . tile ;
vision . oldRadius = vision . radius ;
vision . radius = r ;
UpdateVision = true ;
}
void ChangeVisionXY ( size_t id , Point position )
{
auto & vision = VisionList [ id ] ;
vision . hasChanged = true ;
vision . position . old = vision . position . tile ;
vision . oldRadius = vision . radius ;
vision . position . tile = position ;
UpdateVision = true ;
}
void ProcessVisionList ( )
{
if ( ! UpdateVision )
return ;
TransList = { } ;
for ( const Player & player : Players ) {
const size_t id = player . getId ( ) ;
if ( ! VisionActive [ id ] )
continue ;
Light & vision = VisionList [ id ] ;
if ( ! player . plractive | | ! player . isOnActiveLevel ( ) | | ( player . _pLvlChanging & & & player ! = MyPlayer ) ) {
DoUnVision ( vision . position . tile , vision . radius ) ;
VisionActive [ id ] = false ;
continue ;
}
if ( vision . hasChanged ) {
DoUnVision ( vision . position . old , vision . oldRadius ) ;
vision . hasChanged = false ;
}
}
for ( const Player & player : Players ) {
const size_t id = player . getId ( ) ;
if ( ! VisionActive [ id ] )
continue ;
Light & vision = VisionList [ id ] ;
MapExplorationType doautomap = MAP_EXP_SELF ;
if ( & player ! = MyPlayer )
doautomap = player . friendlyMode ? MAP_EXP_OTHERS : MAP_EXP_NONE ;
DoVision (
vision . position . tile ,
vision . radius ,
doautomap ,
& player = = MyPlayer ) ;
}
UpdateVision = false ;
}
void lighting_color_cycling ( )
{
for ( auto & lightTable : LightTables ) {
// shift elements between indexes 1-31 to left
std : : rotate ( lightTable . begin ( ) + 1 , lightTable . begin ( ) + 2 , lightTable . begin ( ) + 32 ) ;
}
}
} // namespace devilution