Tactics RPG Devlog

Designing a Vision System

Here's some stuff I'd like to happen under a vision system.

You round a corner and come face-to-face with a snarling ogre!

You cautiously inch through a dark hall, it's walls barely illuminated by your feeble torchlight. A pack of ghouls rushes out of the dark!

You walk by a pile of rubble. From the faint torchlight nothing seems amiss, but when you turn your back a monster jumps out and attacks!

You charge toward the monster, but one of the floor tiles you stepped on was a secrete switch! An arrow shoots out of a bolthole in a nearby wall!

You inspect a section of wall and notice a secret door!

Leery of a nearby pile of rubble, the priest casts a light spell to illuminate the darkened crevices. This reveals a giant spider lying in wait!

The general themes here are risk, surprise, and preparation.

Implementation Goals

Here's the mechanics I need to make the fantasy happen:

Line of Sight

Easily done with a ray-casting algorithm based on Bresenham lines. This is standard roguelike stuff.

Variable Vision

Easily done by giving each unit a vision: Int field that determines how long their line of sight is.

Seeing a Tile != Seeing the Entity at the Tile

Each entity (e.g unit, switch) needs an obscurity: Int field and we need a formula: visible(distance: Int, vision: Int, obscurity: Int) -> Bool that determines whether an entity in your line of sight is visible.

Light and Darkness

A lit tile should be visible if you have line of sight, no matter how far away it is. A lit tile should probably penalize the obscurity value of whatever entity occupies it.

I think for this to work vision represents how far you can see in the dark, i.e. how many tiles you can see beyond a light source's radius.

Designing Formulas

We need 2 formulas: one to check if a tile is visible and one to check if an entity at a tile is visible.

Tile Visibility

a tile is visible if:
  it is in your line of sight AND
    it is lit OR
    it is within your vision radius OR
    there is a bresenham line from a light source to the tile
      with length <= vision + light source radius.

That last check is super important but also kind of expensive. I haven't even decided how to model light sources yet.

Entity Visiblity

an entity is visible if:
  the tile it's on is visible AND
    obscurity == 0 OR
    obscurity + distance < vision * light_multiplier (1 if not lit, 2 if lit)

I like this cause:

Implementation

Light Algorithm

We can simplify the vision checks if we have a matrix of light values.

light_value(coord) -> Int
  0 if lit
  X if if the closest light source with a bresenham line to coord is X tiles away
tile_visible(from: Coord, to: Coord) -> Bool
  check_bresenham(from, to) &&
   (distance(from,to) <= vision) || light_value(to) <= vision)

Clean!

entity_visible(from: Coord, to: Coord, obscurity: Int) -> Bool
  check_bresenham(from, to) && 
    (distance(from,to) + obscurity <= vision || light_value(to) + obscurity <= vision)

Also clean!

Computing the light matrix.

record Light { coord: Coord, radius: Int }

for x in 0..MAP_WIDTH
  for y in 0..MAP_HEIGHT
    light[x,y] = INT_MAX

for light in lights:
  for x in 0..MAP_WIDTH
    for y in 0..MAP_HEIGHT
      c := Coord(x,y)
      if check_bresenham_line(light.coord, c)
        value := (distance(light.coord, c) - light.radius).max(0)      
        light[x,y] = min(value, light[x,y])

let's use N for map width, map height, number of lights, then the time complexity is about O(N^4) for N ~= 16.

The lighting grid will have to be re-created under the following conditions:

I want to have light spells that create temporary lights, and possibly a light value on each unit to represent a unit carrying a torch or something. I think this algorithm acceptable for the workload I have in mind.

Data model

Lights would be easy to implement as components in an ECS.

Let's say

alias EntityID = Int
record Position { id: EntityID coord: Coord }
record Light { id: EntityID, radius: Int }
record Unit { id: EntityID, glyph: Glyph, vision: Int }

record Entities {
  position: Map[EntityID, Position],
  ...
}

record Tile {
  glyph: Glyph
  transparent: Boolean
}

alias Map = Tile[MAP_WIDTH][MAP_HEIGHT]
alias LightGrid = Int[MAP_WIDTH][MAP_HEIGHT]

Conclusion

My goal before the next post is to implement the lighting system described above.

Criteria: