A little bit about locking

Let locking be the means to prevent use of some sort of device

Things that can be locked:

  • Door (use = open)
  • Chest (use = open)
  • Magic portal (use = enter)
  • Fountain (use = drink)

Types of unlock conditions

  • Have XYZ key in inventory [active] [E]
  • Actor being of a specific race, or having XYZ traits [E]
  • Have all fragments of the key in inventory [active] [E]
  • Time being midnight [W]
  • A pressure plate being pressed [W]
  • A lever having being pulled [W]
  • A creature being dead [W]
  • Any combination of the above, etc

Entity and world conditions

The unlock conditions are grouped as [E]ntity conditions, where the entity that tries to unlock should evaluate those to true, and [W]orld conditions, where when the state of the world changes with respect to the given conditions, the lock might activate/deactivate.

An actor can’t explicitly lock or unlock a door with world conditions, as they need to change the state of the world in order to get these doors locked/unlocked (e.g. leave an item on a pressure plate, etc)

Active and passive entity conditions

[Active] entity conditions are ones that sort of require the user to do something, if we play out the scenario. Obviously no door opens because a key is in our pocket, but if it is in our pocket and we handle the door, and the door is locked, we can imagine us getting active and using that key in the lock.

Passive entity conditions don’t need the actor to do something explicit. The actor handles the door, but the check happens in the background. The main difference gameplay-wise is that if there’s a lock with a passive condition, after unlocking, the actor can’t lock it again, as it is beyond the actor’s actions.

Back-door

Locks can specify an unlock direction, so that if we somehow ended up in a locked room without the key (e.g. stepped on a teleporter trap), we can open it from the inside. Doing this bypasses all conditions, entity or world conditions. If we do have world conditions, the next time any of the condition changes (to any value), the lock will get enabled/disabled appropriately

Here’s a video that shows a scenario of a lock with world-state conditions only: 3 pressure plates that need to be pressed, but we can also unlock from inside

Player movement, levels, objects

Given the field of vision implementation from last time, I decided it was time to test it and make the game a bit interactive, by allowing the user control of a character. This has been really important, as it has forced me to focus on making level transitions, ensuring movement cost maps and visibility maps work ok, ensuring that save/load works correctly, and a myriad of small little things. Below are a few videos and a list of things done since the last blog post:

Animated and fuzzy fog of war
  • Better fog of war this time, implemented as a simple pixel shader. It’s simple, it’s fast, looks better and does the job for now. The video also shows from about here that now sprites flip horizontally when moving towards the other direction, while when moving vertically they preserve whatever direction they were facing. This costs 1 bit in the 128-bit data per moving sprite, not a big loss or cost 🙂
  • Added functionality for level movement costs (slightly different than overworld moving costs), consisting of background (walls/floor/liquids) and static objects.
  • A* and all other path calculators take into account that diagonal movement is only allowed when the related cardinal directions are passable.
  • Creatures have light sensitivity, and overworld and dungeons have light levels, that affect line of sight radius.
  • I wanted to refactor a bit of the territory system regarding propagation of influence by replacing the data per tile from class (reference type) to struct (value type), so that led to an exciting journey of more changes, fixes, bug discoveries and further bug fixes, and now it seems to be back on track, better, with less code and fewer bugs.
  • Field of vision optimisation that, when the player/sensor moves to an adjacent tile, we only clear the visible data from the surrounding circle with Los+1 radius, instead of clearing the entire map. This of course is not uncommon, but it also had to be done, as now the related performance cost went from 50ms to 0.2ms when the player moves in the overworld, because the refreshed tiles went from 512×512 to 20×20.
  • So far I wanted to have entity “configurations” which are objects that store the exact information to generate an entity, but decided against that due to the cases when entities have to reference other entities during the generation before the entities are created. So now, for example, when procedurally generating a lock and key, I have to create both entities, configure them, put the reference of the door needing the particular key entity to be unlocked, and then call the magic function “EntityBeginPlay” which makes the entity visible to the game, listeners, other entities etc.
  • Level objects (fountains, chests, etc) can now affect movement and visibility. Can now push level objects and update movement/visibility maps appropriately. Also, added doors, and can open and close them at will, blocking movement/visibility. Also, as a fun sanity check, when pushing a fountain in an open door, door can’t close.
  • Explored map disintegrates a bit when revisiting a level. Should later do it based on last visit time. Static objects disappear in explored areas when revisiting a map
  • Save/Load works from overworld and levels
  • Sparse 2D multimaps to store level objects and creatures
  • Ctrl+click moves character towards highlighted path, right click cancels path (this is for fast debugging, should later change with the introduction of turn system)
  • Slightly more flexible sprite rendering, with a list of animations and indices per animation type. So I have a “default”, “moving”, “death” etc animation types, and I can have for example “door closed” “door open” “door locked” as different animations per type.
  • Perlin noise precomputed inverse distribution function and cumulative distribution function. I wanted mainly the IDF, for cavern generation, as I wanted a scalar variable “density” to control how open or claustrophobic a cavern map is. I wanted density to vary in a linear way. My caverns are generated using thresholded perlin noise. But the perlin noise distribution is not uniform therefore the threshold value does not exhibit linear behaviour. Therefore IDF can be pre-calculated and used instead as density, as we feed it a probability value (that can be linear) and we get as output the threshold value to use. So, I did a test with this new variation, and for 10% density to 90% density the final map (after connectivity, etc) looks as follows:

White is open space. The maps get progressively more constrained in a linear way. The last map is very constrained, therefore a lot of parts have been discarded post-connectivity

Here’s a video showcasing:

  • Level transitions and overworld-level transitions
  • Fast-path traversal
  • Pushing objects
  • Opening-closing doors
  • Degradation of explored map after leaving level