Revengate development log

Weekly progress reports about the Revengate development.

Website | sources | Google Play | F-Droid | Itch


I tried Godot 4.3 beta-1 and it looks like web exports are now working on Macs! There are a few things that needs porting, mostly about the type checker being more picky about nulls and the TileMap API changing in 4.3. Half a day should do it. The sampled sound mixer patch didn't make it in 4.3b1, but that does not seem to be a problem with Revengate. I think it's mostly a problem for games that mix background music with sound effects. In any case, it sounds like the Godot devs are trying to get the sampled sound patch in before 4.3 final.

Long rests are now able to deal with competing conditions (like being poisoned). You stop resting if your health keeps decreasing and you are about to die, you keep resting for as long as it takes if your health is recovering slower because of conditions.

Spells obey the setting to disable dynamic lights.

More message tagging and styling.

Plasus rats are more territorial than regular rats.

If there is more than one item at a given location, you see a loot pile icon. Inspect only mentions what the top item is, you need to start looting to see that the other things are.

Strategies have more robust saving and restoring.

Revengate v0.12.8, which contains all of this plus last week's work is out on Google Play and Itch. It should land on F-Droid in a day or two.

Next: a few weeks of camping then I will start exposing special effects and conditions in Inspect.



I had a really bad cold, maybe Covid, after coming back from Belgium. I spend a few days in bed, then I emerged with unexpected mental clarity. I felt able to sit down and write code right away, which is definitely not my usual mental mode. It could be an illusion, or maybe it's the teachings from Deep Work, which I read two weeks ago, that are kicking in.

Or perhaps it's that Revengate is blessed with a truly elite tester: Colin Etienne Jean-Marie Landreau, known as "cwpute" on Gitlab. He writes excellent bug reports and feature requests with lots of screenshots and video captures. I look at those and they always seem actionnable. When I need smalls tasks to get my fingers warmed up, that's always where I go.

With JT, we added spawn_prop to furnishing decks. That enables us to have very rare cards, things you might only see once every few games. We also added a per-game uniqueness rule. This allows us to sprinkle very rare cards in any of the dungeons without running the risk of having duplicate artifacts.

Lots of minor improvements:

  • loot button shows what you are about to loot
  • potions play a breaking sound even when you hit someone with them
  • kobolds have an attack sound
  • ghost death sound plays in full (don't garbage collect the Actor as soon as the visual anim is over); this one still gives me the chill ("HHAAaaaahh...")
  • (short) tap on self is not an action, clear the GUI events pool when that happens rather than queueing a ghost action; long-press on self is still an action
  • new boards are saved as soon as they are generated, before you enter them. This fixes restoring at the auto-save when crossing stairs.
  • the accountant will resolve its Seeking of the Clown Boss as soon as he gets next to him, even if Clown Boss immediately moves (that increases the difficulty of Ch.2 up a notch)
  • yield message when you beat someone is no longer bright red
  • only the last inspected cell is highlighted
  • new talk button in the left bar when someone chatty is whiting conversation range
  • can't long rest if you are reasonably healthy
  • long rest summarizes the health gain rather than one "healed a little" message per HP

I started using Engine.is_editor_hint() to prevent the @tool script from modifying the Godot scene files. This makes my diffs much cleaner! I discovered EditorScript, which I now use to clean up scenes that have been excessively modified by @tool scripts, but sed is still the best tool to remove one-line properties.

I removed all the code from the old Kivy implementation of the game. I was at feature parity with the Godot re-implementation for a while and keeping that old Python code around want not serving any purpose. I'm glad I wrote it, but I'm OK never looking at it ever again.

The next quest is going to be about Pacherrs visiting Lyon. I typed a story I had written a while back to help frame the quest design (scroll down to Drums of Disruption). There's some steampunk magic inside.

Next: a bit more gating to keep diffs clean; loot piles should look different from the top item.



I'm now only saving and loading one board (level) at a time, only keeping the active one in memory. This should improve the performance on older mobile devices, especially towards the end game. I have not done any A/B test yet to see if it actually makes a noticeable difference.

I was puzzled as to why conditions (like being poisoned) would not save, which becomes a bigger problem if the game unloads boards as soon as you leave them. For example, you might toss a few potions of poison in someone's face before running upstairs. You would be in your rights to expect the poison to do some damage and meet to weaker enemies when you come back. The problem was two fold: Godot's ResourceSaver won't save internal classes and the class constructors are called without arguments at load time.

Anything you want serialized has to be the top level class in its own GDScript file. Anything you want to load needs support a no-args constructor (_init()). Godot will set all the saved attributes after doing a blank instantiation.

I did another small perf improvement. When a monster is Exploring, it will look for a reachable waypoint first. I use Dijkstra for that since I want to make sure that the waypoint is going to be reachable. I used to clamp the Dijkstra search by max distance, but that was still slow on big open levels with too few obstacles. I now clamp it by max inspected cells and that allows for picking further waypoints on levels with a lot of obstacles at no performance cost.

I received another 5-star rating on Google Play. Thank you, kind stranger! Those always energize me for several days. You probably can't see the star rating on Google Play, unfortunately, because Google wants a lot of ratings to unsure statistical significance before they make them public. That or Google wants to support the work of all those struggling click farms out there.

Next: add a setting to disable soft shadows.



I wrote a talk proposal for Roguelike Celebration. It's not submitted yet and any feedback on how to make it more compelling would be greatly appreciated. You can put your comments directly in the document.

I worked with JT to tag messages and give them different styles depending on their category (combat, vibe, inventory, ...). They are all muted pastel colours, but it might be too colourful for some players. I think the proper long term solution is a setting to say which messages you want to see on the main screen and which one gets silently archived in the message screen that you access with the book icon. We're using Godot themes for the styling, the UI is a bit rough around the edges, but the API is nice.

The Monte Carlo simulator for decks now includes vibes and items in addition to monsters. It was a good time to improve the nomenclature: "sim", "stage", "per depth counters" is easier to conceptualize than "sim", "sims", and "all_sims".

It averages 18k fully generated and drawn decks per second. Turns out that using a Node instance for each card and duplicate()'ing it at draw time is very cheap in GDScript.

I stated the refactor to make the game unload inactive levels. I didn't write much code besides a refactor to make card tallies easier to serialize, but I think I though of all the edge cases and that should actually not be too big of a change. This should really help the perf on low end devices.

Next: finish unloading inactive levels, tag a few more messages.



Changes to perception are now animated and you can long-rest by holding the skip turn button. Both together feel great! This also makes it easier to see how the potions of booze and absinthe are affecting you.

New items: potion of coffee, bioluminescent mushrooms, eye glasses.

There is probably something smart to do with the long press on the quick attack button, either toss the current weapon or open a selection screen to equip a new one. I might try both since I can't make up my mind on which one feels the most intuitive.

Next: I think I'm ready to unload inactive levels to make the game behave better on low RAM devices.



We have non-rectangular rooms!

I converted a subset of Zorbus room templates to json (see last week's update to see the whole set). I save space by only keeping the "pillars" of the wall surrounding the room: the cells where the wall changes direction. I don't keep all the room templates since many are too big for the typical Revengate level.

I thought I would use Primm, but I tried the new templates with my current binary space partition and it looks really good! I might still do Primm, but much later. After slicing the board, I try to pick random templates that fit the partitions. The probability of getting a boring old rectangular room is adjusted depending on where you are. Rectangle still fits better for the houses on the surface, for example.

Next we have to decide where doors can go. This turns out to be really easy: anywhere along the perimeter, except where there are "pillars".

This is out on Google Play and Itch. Should land on F-Droid in a few days.

Next: new items.



Not a ton of progress this week: I started revisiting my level generator. I changed the passage carving on my BSP levels to use A* with constraints rather than dumb elbows. In most cases, that makes beautiful slightly twisty passages. There are edge cases where the passage will make a big detour around a room, this will go away once I move away from BSP.

Resurrecting the Dungeon Exhibit was really helpful. It's a minimalist version of the game without monsters. It has quick transition from one level to the other with arrow keys and it can regen a level with a single key, which makes it easy to test how well I am handling various constraints.

I wrote an importer for the Zorbus prefab templates (big image). I plan on using those instead of rectangles and grow the level with a modified Primm algo, just like I do with the maze levels. What is left is figuring out something sensible for door placements.

Next: finish the new level generator, close the plot hole with the code loom cards.



Lots of eye candy this week!

Instead of showing a message with the number of turns to arrival while traveling, I now highlight the path to be taken. It's oddly satisfying to see the path auto-update when your character notices a new obstruction. It's a bit distracting to see a diagonal step pop out of nowhere – A* artifacts that would be expensive to completely get rid of. I also added a debug flag to highlight the paths of all the other actors. I think this will become quite handy if I want to fine tune strategies like Swarming and Guarding.

We now have dynamic lighting.

The experiments I did with glow a few weeks back were a dead end. The glow intensity changes too much from one Godot renderer to the next and since the game aims to support Desktop, Web, and Android, that was a no-go. Dynamic lights on the other hand behave beautifully similar on all renderers. There is still room for adjustments and this will only achieve its full potential once we can pickup light sources. Thankfully, Godot lights ignore the visible property and use enabled instead. That will make it very easy to make items that keep emitting light even after their glyph become invisible as they become part of your inventory.

Tuning the lights forced me to invest the time to finally understand blending modes. I get it now! It's easier if you remember that the name implies that each channel is a float in 0..1. "Add" and "Subtract" are fairly intuitive. "Multiply" darkens the scene because the channel of what you are drawing and whatever was there are 1.0 max, most likely 0<=x<1, so the result will be smaller than what you started with. Similarly, "Divide" blending brightens the scene because you divide by something smaller than 1.

There is also a new wait-and-heal command. You rest, but to balance that being too easy and tempting to do all the time, you do it with your eyes closed so there is a risk that you'll be surrounded when you resume your exploration.

The above is on Itch and Google Play, F-Droid will take a few more days to catch up with the update.

Next: portable light sources that don't behave like they say on the package (rogue lights), draft the main lines of the next quest.



My game stopped building on F-Droid. Thankfully, the team was kind enough to open a ticket in my gitlab to let me know. They also have some fancy tagging system that will watch the issue in my project and restart the build when I close it. It turned out to be minor bug in the Godot Dialogue Manager plugin. Updating it solved the issue.

The harder problem, the game crashing during visual effects on some older Android devices was not as easy to solve. I still can't get my head around it. There can be fairly fancy shaders, but they can't take scalar uniforms. I reworked the spells so they would look good with only particle systems and Tween animations. I added a config to let players turn off shaders if they happen to be on a device where those crash the game.

When shaders are enabled, I now pre-load the spells and special effect sub-scenes during the wall of text game intro. This completely removes the stutter the first time you see a spell.

Based on an excellent bug report, I reworked the dialogue pane to make it more obvious to the player how they can interact with it. Tap in the text area advances (finish typing or start the next line), tap outside of the text area closes the conversation, and possible responses are now buttons rather than text line of a different colour. The old Close button that was easy to miss since it was hidden under your thumb has been moved to the top of the dialogue pane.

Next: highlight the path while traveling, start fleshing up a 4th quest.



I'm in Europe again: Toulouse, Carcassonne, Marseille (now), Lyon (Revengate is set in Lyon), and Brussels (beer!). I left myself get distracted by touristy things and the on-the-go lifestyle of fast city hopping made it hard to develop a good coding routine. It's way easier when you have a desk with the whole setup and you can just sit and get going.

I also have a really hard time with jet lag. I know the theory, but in practice, I'm just up at 4am for days. I think I'm good now, waking up around 8:00 this week.

I tidied up the story in chapter 3 of Revengate. There is a whole cast of NPCs who will hint at the background and what they have to say evolves as you become more aware of the big boss.

To make it easier on myself, I improved the summary of what actors know in the debug inspector. I really like the metaphor of learning facts, which you automatically forget after a while depending on how crucial that information is. It feels natural to write branching dialogues with that building block.

I thought that conditions (poisoned, on-fire, ...) were saved, but it turns out they are not. I tried getting to the bottom of this, but I couldn't find the problem. It's probably some subtleties with how Godot manages Node.owner that I don't fully grasp. I think I will convert those from Node to custom resources. The latter is harder to add on an actor in the Godot editor since you can't just drag and drop them, but they always save and restore how I expect them to.

I changed my shell to fish and added atuin for history management. Atuin converts the shell history from a flat file to a database with lots of metadata. This means I can how have a super charged CTRL-r with multiple facets to search for. It's great when looking for a command I vaguely recall typing a while back, but it get even better. I can also see the history of a given command: how often I type it, how long does it usually take, what the return code was the last few times I used it. A like how interactive fish is and how clean my setup file is. I don't know if it makes me more productive, but it makes me happier for sure.

Next up: find out why the explosions crash the game on some Android devices (thankfully I own one of those) and rework the dialogue UI to make response selection more obvious (probably something closer to what interactive Japanese novels do).



There is a new zapping spell. The effect takes too long and I would rather have a long bolt that goes from the caster to the victim, but since I can't figure out the proper way to do that right now, the shower of lightning particles will have to do for a little while.

It turns out that the missing noise texture for the animated water on F-Droid is indeed a Godot bug. Not converting resources to binary is a perfectly fine workaround and it barely increases the size of the APK. Maybe it slows down loading, but I can't tell. In any case, it should be fixed in Godot 4.3.

The accountant has new things to say after the confrontation. He also becomes neutral after you apply enough "convincing" with your favourite blunt object.

Next: give aggressive monsters a bit more smarts on how to corner the hero.



I wrote a new heap queue, fully typed and optimized for working with 2d coordinates. Compared with the old one, it does about 3x as many operations per second. This comes at the cost of losing some generality and doing some voodoo with bit shifting rather than divisions, which I borrowed from the Python heapq module. Using this new queue, I can do about 40% more A* path findings per second.

This is probably as good as it's going to get. The best thing to do now would probably be to either recycle paths with D*-lite or do sub-optimal paths with Best First Search when there are many actors in play. I could also simplify perception rules and that would shave a whole lot, but I kind of like that monsters can hear/feel you when you are just out sight around a corner.

The pinching gesture now uses the centroid of all fingers as the zoom focal point. This feels so much better! It used to have the top-left corner as the focal point and I was looking for a matrix transform that would do everything in one go, then I figured that I only had to zoom then see how far that moved the centroid, then translate the camera by the opposite of that offset.

The hero can learn spells. That UI is going to suck passed six or seven spells, but since there are only a handful of spells and even fewer spellbooks, it will do for now. The attack button should stay on the far right, but that we need Godot 4.3 for that. Stay tuned!

All of this and the tossing of items has been released in Revengate v0.12.0, now live on Google Play and Itch. It should be up on F-Droid in the next few days.

Next: hide spell books in dangerous places.



I was working on perf this week. Based on the good leads we received from players the week before, JT Wright and I used the Godot profiler on the game's Monte Carlo simulator to add caching on the stats modifier and also make sure that it does not change the general outcomes of encounters. This brings about 50% more combat turn/second for about 12 lines of code. Not bad!

I added a big room for stressing movement, something that was hard to stress test with the combat simulator. Indeed, entering that room was producing a noticeable pause on mobile. In order to find a suitable waypoint for Exploring actors, I used to run a pass of Dijkstra on the whole board. This had the benefit of guaranteeing that the waypoint is reachable, but it proved to be too expensive for what we needed. I now limit this first Dijkstra pass to a roughly 10-tiles radius and that really helps. Going towards the waypoint is using A* and that is considerably faster, especially when there are no obstacles.

This week, I learned that unlike Python, GDScript is faster when you provide types and indeed, turns are about 30% faster in the large open room after fully typing my distance functions. The Godot profiler is suggesting that I really should add some types the priority queue that my path finding is using. I sadly noticed that the git repo for that plugin has been taken down and now I'm really glad for open source and my ability to take over the maintenance. I'll see next week if specializing and typing the queue improves things, then I will consider if the API is still general enough to be worth publishing in the assets store.

I wanted to work on spells but I ended up doing very little. I added a spellbook that teaches you casting skills and factored out the health meter so I can be restyle it and use it for mana. There is still no UI for the player to cast.

Next up: stop optimizing once the refactor of the priority queue is done and then make the UI for casting.



You can now throw things. Potions auto activate upon shattering, weapons not designed for throwing do less damage, some weapons like the hammer have a two ranges: full damage nearby, half damage far away. A lot of this was done in collaboration with JT Wright.

I moved to the kitty terminal two weeks ago and I really was not sure if that was worth the effort. It has really nice shell integration, but very little discoverability so you have to read the doc to know what you can do. This week, with hyperlinks and mimetypes properly configured, it became clear that my terminal was now the a great place to do sound design.

I moved my durable task tracking to todo.txt. At first I thought that the requirement for a task to completely fit on one line would be limiting, but then it soon became clear that I was using my old TODO app for unactionable notes. Now I have two clear systems that do what they are meant to do and do it well. I like that I can do everything in my text editor and that I don't need 7 clicks to add 3 items to my grocery list. I still track short term items on paper.

A player used the new cheats to nail down the cause of a perf regression introduced a few releases ago. I'm amazed and energized seeing their dedication to this cause!

Next: the hero should cast spells.



Added a setting to change the size of text controls. The most logical size is still auto-detected based on screen size and pixel density, but who am I to tell you if you should enjoy squinting or not.

Added a cheat to conclude a chapter.

Finished the refactor of the inventory screen to make it stylable. Scrolling is better, but it's still hard on a small screen. This will need more work. My reference right now is Spotify: it just knows the difference really well between scrolling a playlist and selecting a song.

Added a long description on all the items. Added a "secret" stash area where you can explore the previous point.

Released the cheats and the settings screen in v0.11.6 to Google Play and Itch. It should be picked up by F-Droid in the next few days.

I started working on a targeting system so you can finally toss those dynamites rather than carefully placing them on the ground before (hopefully) retreating. Be warned, folks, the fuses are about to get a whole lot shorter. If I get this slick enough, that's pretty much how I know we're ready to teach the hero some magic.

Does it seem obvious that the blue highlighted tiles are within tossing range?



The water bug on F-Droid is almost certainly a Godot bug, but I have not made a minimal repro yet to report it upstream. I did however start detecting if the noise textures are missing in order to gracefully fall back to static water tiles.

This and last week's work has been released in v0.11.5.

I started the redesign of the inventory screen with JT Wright. One awesome side-benefit of no longer relying on static images for buttons is that we don't have to make one button do many different things. You "activate" a dynamite and "consume" a potion. Much better! Besides that, we are just shooting for feature parity and skinnability. Cool features like filtering and sorting will come later.

We tried MacOS exports. They work just fine, so I will probably start publishing those as well.

All items now have a "simple" and a "detailed" description. You need very high perception to see the detailed one – lots of excellent hints hiding in there.

I added a settings screen. The only settings so far is to enable cheats. I don't mind players cheating if the game is too hard and they are completely stuck. I mention that they cheated on the victory screen – I don't judge, but you should not exaggerate your bragging rights.

Next up, polish up the cheats that I have been using for testing so that they are not too confusing in the hands of players. Add a few other settings.



I played with UI and UX this week.

The text controls are now much bigger on small screens. The advertised screen DPI is not accurate, but I can still use it do divide screens in three classes: tiny, small, big. I make buttons and labels bigger as screens get smaller, text margins follow an inverse rule. I will have to redo the inventory screen since the buttons in a Godot Tree control are not stylable.

Godot has a cool feature: theme.merge_with(other_theme). That way I just need mini alt themes for the screens sizes that need bigger text controls. The UI to set those properties is far from intuitive, but the way the controls just stay where I want them to be makes it very tempting to say that I like that system better than CSS. I will probably be certain by this time next week.

Following a player recommendation, you can now hold the arrow keys down and you'll seamlessly keep moving or attacking in that direction. It's quite satisfying! I was already doing all the state management to only pass commands to the hero when it's its turn, so it was only a matter of looking for key-down rather that key-released.

I'm trying to see why water is not animated in my F-Droid builds. It's weird! I think I will soon have narrowed it down enough to declare it a Godot bug.


New year, new platform: you can now play Revengate in the browser! Godot web exports do not work yet on M1 Macs, might only work with Firefox on non-M1 Macs.

This was easier than I was expecting – I added the web export recipe and then almost everything just worked, including saved games across multiple browser sessions. Thanks Godot! I just had to tune a little bit my auto-resizing setting and do a dirty hack to make sure stale saved games are not exposed to the player (deleting entries from the Local Storage seems less reliable that creating them).

The controls still very much have a mobile flavour to them. Adding ways to do things with mouse and keyboard is going to be the theme of the next few releases. For now, I made sure you can cancel multi-turn action and close most dialogues with ESC.

I added two new potions: potion of absinthe and potion of analysis paralysis. Both serve some of the purpose of the scroll of identify that is common in roguelikes. Since they come with some drawbacks, they introduce some fun dilemmas.

I wrote a brief recap of my progress during 2023 for the RoguelikeDev subreddit.

Next: figure out why water in not animated on the F-Droid builds.


Older Updates