Revengate development log

Weekly progress reports about the Revengate development.

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

2023-12-29

A player reported a regression with the latest release and they were kind enough to provide a video of their experience to illustrate the situation better. I'm just floored that someone decided to invest that kind of time in making my game better rather than moving on to the next game. Just seeing the bug report made my day!

It turns out that auto-saving at every turn might not have been the smartest way to go about it. It's absolutely unnoticeable on my desktop and works fine on my beefy phone from 2019, but on a more modest phone from the same year, it makes the game unplayable. So now I only save 10% of the time (at random, this is a roguelike) and when going back to the main screen. I also moved the I/O outside of the main Godot thread to avoid the impacting the animations. I thought that doing so would require lots of signaling and locking to avoid concurrent corruptions, but then I recalled that the rename() system call is atomic on Linux (which includes Android) and that made the isolation trivial.

You can now see weapon stats all the time no matter what your perception is, but you might get a rather vague value like "good" or "weak" if you are not particularly perceptive. Perception is now on the stats page for all monsters and actors.

I added a long description for all weapons. There are both a low and a high perception description for everything.

I noticed there is a weird bug with the F-Droid export recipe: the Godot the noise resources are not exported and as a result the water is not animated. That seems to be the only problem. I haven't gotten to the bottom of this one yet.

Next up: potion of analysis paralysis and potion of absinthe

I wish you all a new year full of steam and cogs, and perhaps a new fancy hat.

NO BLOCKERS

2023-12-18

I made a demo reel.

It's not super exciting, but it show cases the main elements that make Revengate a traditional roguelike with a special twist. I used Flowblade for the first time and I quite liked it. As video edition programs go, this one has a very simple UI that is perfect for simple cuts and sequencing.

I paired with JT on making movements considerably faster when there are multiple visible actors.

All bot movements are eligible to start at the same time, but we stagger them instead to give a better sense of the initiatives. With the staggering, bots really feel like they have a mind of their own. Without it, turns feel too mechanical. The new approach takes into account how many actors are visible to decide how fast the animations will go and how much of it should overlap.

JT added the UI to see the items description and stats from the inventory screen. I will have to add more long item descriptions before this reaches its full potential. JT also added a button to get a summary of the active quest.

Saving and restoring games work!

Roguelike Celebration has posted the videos from the 2023 edition. My favourite were:

These were also good:

Next up: a bit more cleanup, then push the above new stuff to Google Play and F-Droid later this week.

NO BLOCKERS!

2023-11-18

I started working on saving games. Godot custom resources are somewhat similar to Python pickles: they can both save arbitrary native and user-defined types, and they both suffer from arbitrary code execution vulnerabilities. For Revengate, the latter not a big deal because it's hard enough to upload files inside the Android sandbox that no player is ever going to try using saved games that they got from shady places on the internet, and even if they did, the malicious code would not be able to do anything outside the sandbox since Revengate requests no Androids permissions at all. As long as we don't have a desktop version of the game, that's a productivity shortcut that I'm very comfortable taking.

Another big pro of custom resources is that new fields can have a default value, which provides some level of forward compatibility. It's not perfect, but it covers many cases. For the rest, I can only offer the player to delete their old saved games because Android won't let them downgrade the game even though I publish all the old APKs (well, it's doable, but it's really hard).

Custom resources work amazing for flat data, but you need an extra twist to make them deal with data trees. Thankfully, Godot also has PackedScenes for that. Basically, that's how the editor saves your scenes as .tscn files. And that is also where the similarity with Python pickles end because there is a fair bit of magic with how Godot decides what nodes should be part of a packed scene. Thankfully, I found this very helpful diagram. Basically, the owner of a node has to be inside the PackedScene or has to be in its parent chain. Fair enough, but Godot will also void the owner field for various reasons, including when you reparent nodes, which is how the hero moves to between levels and how actors pick up items in Revengate. So long story short, I think that the best way to do it is to recursively set the owner to the root node of what I'm saving just before the actual save. That seems too work, but I'm aware that I might be forgetting about a few edge cases. And the beauty here is that everything is included: poisoning conditions, bot goals, memory of who offended whom... Everything!

In any case, I'm way further on this than I thought I would be after one week, which is really good for my motivation!

Next: figure out how to tell the rest of the game about a restored state. That is, reconnect all the UI signals and refresh the view.

NO BLOCKERS.

2023-11-10

Swarming works! I tried to come up with a demo gif, but since most of the magic happens out of perception range, you'll have to take my word for it.

Swarming monsters seek each others. If they have enough connectedness, they might seek other actions, such as exploring the level. Only one in the swarm that is exploring is enough to move the whole swarm. If a higher priority action kicks in, such as kicking the hero's ass, the first the monster to see you will stop swarming, but the swarm will try to stay with it and soon the whole swarm will be after you. This is very different from, for example, giant bees in Nethack or spiders in Caves RL where they all start in the same room, but will readily diffuse after you wake them up.

JT paired with me on this one and I'm happy with how simple it turned out: if you can get closer to the peers that you can perceive, that's what you do, unless you feel adequately surrounded already. I was afraid it would end up as a jumbles mess that requires cross communication between actors, but we found a good way to reduce it to fully autonomous decisions.

I spent the rest of the week running Monte Carlo simulations and play testing until the polish and the balanced felt right. And since I got there yesterday, I pushed v0.11.0 on Google Play. It's properly git-tagged and should therefore appeared on F-Droid in a few days.

This release adds:

  • daggers with a longer range;
  • new building: church;
  • new dungeon: crypt under the church;
  • new weapon: Mjölnir;
  • new monsters: cherub, skeleton, sentry scarab, nochort, plasus rat, Algerian giant locust;
  • new monster strategy: swarming;
  • new item: carrot.

Bug that were fixed:

  • can no longer chat across walls and closed doors;
  • more consistent clearing of action highlight markers (the red and green squares around actors);
  • monster strategies now obey perception (with select rare exceptions).

Player experience changes:

  • single tap on distant actors pop a context menu: (get closer, inspect, ...);
  • added a health bar;
  • flash a message on with the reason why a travel command aborted (path block, under attack, ...);
  • Quick Attack button is available as soon as you've attacked someone in range, even a friendly actor.

Next: items descriptions and saved games.

NO BLOCKERS!

2023-11-03

I paired with JT Wright who made a really cool Monte Carlo simulator for the furnishing deck builders. Revengate populates new game boards with monsters, items, and vibe by making a "card" deck of what could go there based on rules, what was previously generated, and budget. Then we draw cards until we meet our budget for the level (or the deck is empty).

The new simulator runs 1000 whole dungeons and then outputs a statistical summary of what has been drawn. For example:

Summary of depth 1
(c) desert centipede:  9.05% | p10: 0 p50: 0 p90: 1
          (k) kobold: 29.09% | p10: 0 p50: 1 p90: 3
          (l) labras: 15.17% | p10: 0 p50: 1 p90: 2
             (r) rat: 11.24% | p10: 0 p50: 0 p90: 1
       (s) sahwakoon: 11.65% | p10: 0 p50: 0 p90: 2
     (o) sewer otter: 13.04% | p10: 0 p50: 1 p90: 2
    (f) Sulant tiger: 10.77% | p10: 0 p50: 0 p90: 1
[...]
Summary of depth 6
(c) desert centipede: 11.38% | p10: 0 p50: 1 p90: 2
           (𝔤) ghost:  9.62% | p10: 0 p50: 1 p90: 1
          (k) kobold: 11.20% | p10: 0 p50: 1 p90: 2
          (l) labras: 19.81% | p10: 0 p50: 1 p90: 2
             (r) rat:  9.26% | p10: 0 p50: 0 p90: 1
       (s) sahwakoon: 10.13% | p10: 0 p50: 1 p90: 2
     (o) sewer otter: 15.30% | p10: 0 p50: 1 p90: 2
    (f) Sulant tiger: 13.29% | p10: 0 p50: 1 p90: 2
[...]
Summary of depth 10
(c) desert centipede: 13.95% | p10: 0 p50: 1 p90: 2
           (𝔤) ghost:  3.69% | p10: 0 p50: 0 p90: 1
          (k) kobold:  4.51% | p10: 0 p50: 0 p90: 1
          (l) labras: 23.33% | p10: 0 p50: 2 p90: 3
         (P) pacherr:  3.22% | p10: 0 p50: 0 p90: 1
             (r) rat:  8.46% | p10: 0 p50: 0 p90: 2
       (s) sahwakoon:  9.36% | p10: 0 p50: 1 p90: 2
     (o) sewer otter: 18.29% | p10: 0 p50: 1 p90: 3
    (f) Sulant tiger: 15.19% | p10: 0 p50: 1 p90: 2
[...]

It reads as follow: kobolds represented 29.09% of the monsters generated at depth 1. In the 10% time with the fewer kobolds, there were none, the median number was 1 at this depth, and 10% of the times there were 3 or more.

As you go deeper, you start to see ghosts. Since ghosts have an extra rule that makes them very rare (just 3 per dungeons), you are less likely to see them at depth 10 since we don't have any ghost cards left at this point. This was not previously the case as the rarity of ghosts was entirely controlled by their high spawn cost, but the simulator allowed us to fine tune the generation with the extra card rule.

I added a few monsters and put then in the new deck for the crypt dungeon from last week: plasus rats, skeletons, and the scary cherubim. I used the deck simulator to make sure that the scary cherubim were adequately rare.

A few other monsters: nochorts, sentry scarabs, Algerian giant locusts.

I tried to make the giant locusts move as a swarm, but my naïve approach is not cutting it. The applicability of the strategy seems too rigid and the locusts fall out of swarming too easily. I want them to pass the control to other strategies for attacking and exploring, otherwise the swarming strategy needs to re-implement everything in the context of swarming. I really like the idea of swarm emergence, so I will experiment more on this one.

Next: maybe items descriptions, maybe I polish what I have and push a new release out. I'll see how I feel tomorrow morning.

NO BLOCKERS!

2023-10-27

The new crypt under the church is a series of long corridors with small chambers on each sides. This is strikingly more organized than traboules and more claustrophobic than the surface since it's so easy to have your escape path blocked off.

PreFabs can now emit cards that will go into the level furnishing deck (Actors, Items, Vibe). Cards can be either mandatory (you really want a priest in the new church) or probabilistic (might smell of incense, or maybe not). The spawn cards now have an optional spawn_rect field to restrict where they can appear.

I helped Victor Pernet to add his game (Pirate Solitaire) on F-Droid. He got the whole thing to build clean, we are now waiting for the F-Droid team to audit his code to make sure that it is as Open Source as he claims. That will make Pirate Solitaire the second Godot-4 game on that app store.

Roguelike Celebration was awesome! The talks have not been posted online yet, so I will probably repost this when they are. My favourite ones were:

  • Fireside chat about the development of NetHack by Jesse Collet, Keni
  • Audible Geometry: Coordinate Systems as a Resource for Music Generation by Paul Hembree
  • Backpack Hero - Player Upgrades and Progression by Jasper Cole

I also really enjoyed the unconferencing.

I tried to emulate the focus mode that I had near the Merced by removing the wifi password from my laptop. This way, I can only access the internet when tethering with one of my phones. When I want to focus, I put all the phones in a different room. It's too early claim any kind of victory, but I like how simple that was to implement.

Next: add the right vibe cards to the church, populate the crypt with undeads and scary angels.

NO BLOCKERS!

2023-10-20

I managed to do a few things while camping:

  • added a health bar
  • church prefab
  • single-tap on a distant actor now pops a context menu, there is a new command to get closer (within action range) to them
  • Traveling multi-turn command flashes a message on why it aborts (attacked, path blocked, ...)
  • quick-attack button is available on friendly actors you've previously attacked
  • carrots
  • action highlight are properly cleared after a conversation, now using the is_valid() predicate of the Talk and Attack commands.
  • Healing monster strategy is now triggered by health-% rather than health points
  • TribalTerritorial monster strategy is taking perception into account
  • magical is now an Effect tag rather than a field

I felt really productive even though my coding was timebox to only a few late afternoons. It's weird, sitting there at a wobbly picnic table that was the wrong height, without my mechanical keyboard or my external monitor, code came easier than when I have a perfectly ergonomic desk setup. It took me a few attempts to get there. It seems like best coding spots were the ones where I didn't get any cell service. The calming sound of the a river helped too.

Yosemite is full of potential distractions: climb something, watch people struggle on the Generator Crack, refill my water jugs at Fern Spring, hang out with a beer in El Cap meadow while watching big wall climbers with binoculars. All those, however, have a fairly high activation cost, which puts then in big contrast with checking the news or going down a Wikipedia rabbit hole. I can obviously put my laptop in airplane mode when I decide to work on the game, but it's so trivial to override that commitment that I don't think it's going to work as well as sitting by the Merced river. It's also possible that the legend is true and that there is something magical about the Fern Spring water, so just to be safe I stocked up on several litters of the stuff.

2023-10-13

I made up my mind on what should go in v0.11 (you might have to scroll down, gitlab is weird). In short, it's mostly more work on UX but you still get a few new monsters, including the very scary cherub. I don't want to add new quests until it's possible to save and restore games, the cherub will therefore sit in a purely optional side-dungeon.

I upgraded my DialogueManager plugin for Godot. My speech bubble was doing integration based on the plugin internals rather than the doc, so I was afraid things would break. It did, but the public API now exposes all the signalling I need so it was well worth doing this cleanup.

2023-10-06

The threat of a government shutdown tricked me into changing my camping reservation in Yosemite. I lost a few good climbing days, but that gave me time to fix all (known) bugs in Revengate v0.10.0 and to push it to Google Play. It should also show up on F-Droid in a few days.

This includes the new quest and a few significant UX improvements.

2023-10-01

I made it possible to keep playing after failing a chapter. If you don't succeed a non-blocking quest, the quest NPC is a little pissed and you don't get a quest reward, but you can keep playing. This required decoupling the victory logic from the victory and game over screens. It was also the right opportunity to move "quests", which were mostly scattered conditions, as their own self-contained entities.

JT and I added a collection of surface areas to reach the new quest. This is pretty much a nice city stroll with lots of friendly NPCs and just a few rats and thieves. It's good to take a break from the non-stop action. We had fun writting the rumors and chatter you hear around town.

I'm heading out camping for a few weeks, so probably no more updates until after Roguelike Celebration. I was hoping to get the next build out before camping, but that won't happen.

2023-09-24

You can now offend someone from a conversation and someone who recalls being offended becomes hostile. This decays over time based on the severity of the offense. The new quest has variable rewards depending on if you stick to beating up the informant or lose control and kill them. Better go with a weak weapon if you happen to hit pretty hard.

There is a new monster: the yarohu.

Worked with JT on balancing the new monsters and NPCs for the new quest. The Monte Carlo simulator was great! It still has a few rough edges, but it already can summarize hours of test play in just a few minutes.

I fixed the scrolling to the hero after a quest victory and after entering a new board.

2023-09-15

The game finally landed on F-Droid. The build recipe is pretty straight forward: build Godot for Linux, then cross compile the export templates for arm. The tricky part is that Godot is missing a bunch of import metadata until you start the editor. Since I can't do that in the gitlab CI/CD, I export the game twice and that seems to solve all the problems.

I worked on placement constraints: for the next quest, I need to place two NPCs as far apart as possible to give the player a chance to prevent them from meeting. I got the region based constraints working (I can tell the monster deck generator that some monsters should always spawn on the North side of the map, for example). I should get the distance constraints working tonight.

I watched the preview event for Roguelike Celebration and the interview with the Diablo lead dev was so good!

2023-09-02

Revengate got it's first 5-stars review on Google Play!

Thank you, kind stranger.

It's now more obvious when you started a multi-turn travel. It's should also be more obvious how to cancel if a fat-finger-tap is what started the action. Maybe a bit of blur under the text would help with legibility.

Drag-to-pan is no longer super fast when zoomed-in.

Working on the new quest: Le Grand Salapou, the master mind clown spy is about to meet with an informant and you have to stop the encounter before it happens. Salapou is neutral, but easy to anger and his entourage tends to follow his lead.

A few new mechanics were introduced to make this quest possible. The global cross-faction sentiments are not variable. The Clockwork Automata have a guarding strategy: they follow Salapou around without getting in his way. They immediately retaliate if the person they are guarding is attacked. The informant can seek someone (Salapou in this case) and they will yield if you are convincing enough (that is, if you beat them up). JT wrote those last two.

I discovered ripgrep. Wow! It's awesome! It does a perfect job of only grepping my Godot files without looking at any of the auto-generated stuff. Makes me want to do a ton of refactoring now.

Next up: work on the dialogue and conversation triggers for the new quest.

NO BLOCKERS!

2023-08-25

The quick attack button shows how many weapons you have when you equipped from a stack. The button icon is updated whenever the active weapon changes.

This fixed several usability issues in one go. That was time really well spent!

I fixed the pan: it's no longer super fast when zoomed it. The board is pretty much sticky under your finger, which is what Material Design recommends. It's not quit there yet when doing a two fingers pan. I may or may not spend more time to get that to work as well.

I got the game to build on the F-Droid infra. It's in review now and it should get accepted. Fingers crossed!

The recipe is actually very straigthforward: build the Godot editor for X86, then the Android export templates for ARM (both the C++ and the Java part). If you've never launched Godot in interactive editor mode, it will complain that a bunch of files have never been imported and it will ignore them. Compiling the APK twice in a row fixes that.

There was a lot of fiddling because different parts of Godot want to learn about what you are trying to do from different sources (command line flags, env vars, config files). I will probably open issues with Godot to normalize that a little bit. At least the Godot configs are standard .ini files that the Python configparser module knows how to deal with, for the most part.

There are just a few more minor usability issues to tackle this release, then I add the new monsters.

NO BLOCKERS!

2023-08-19

I submitted Revengate to F-Droid. F-Droid is a very interesting app-store for Android that only catalogues open-source packages. They are so serious about this mission that you can't send them your signed binaries like you do on Google Play – you have to provide build instructions so they can build it from source themselves, including all your dependencies. In the case of Revengate, that includes building Godot as well. Thankfully, their build environment is Ubuntu, which is also what I run on my dev machine. I'm running slightly different version of the OS, but it's close enough that I feel good about what I submitted even though I didn't run the build pipeline on their Docker images. Revengate also seem to be the first Godot 4 game on F-Droid.

UX is my least favourite part of game development, but seeing a few testers struggle with their first exposure to Revengate motivated me to spend the next two releases on smoothing things out a little bit.

I did my best to identify and quantify the current interface problems, then I scheduled the most promising solutions in my roadmap. Of course I can't only do UX – that would be a reliable way for me to burn out – so I also plan on adding a few new monsters.

I started working on the quick attack button with JT Wright.

It indeed streamlines combat! It's the same number of taps as tapping on the monster, but the new button is always right under your thumb and you don't need to aim.

NO BLOCKERS!

2023-08-12

The monsters Tracking strategy is now triggered by perception: a monster will not start tracking a foe until they can perceive them. They will keep tracking for a while after loosing perception, but they stop tracking if when it's been too long since they sensed their prey. If you have better perception then them, you could retreat ASAP when you sense them and thereby avoid them entirely.

Most monsters are roaming, so you still bump into then, but at a lower frequency. This feels really good and it pushes the player to balance the trade-offs of side exploration vs direct path to the next dungeon level.

I smoothed most of the rough edges of items grouping with JT Wright.

I published v0.9.3 on Google Play. It includes this, the ranged combat from last week, and the healing spell.

Next up? I think it's time to add a new quest.

NO BLOCKERS!

2023-08-04

Similar items in the inventory screen are now grouped together.

There are still a few edge cases to take care of, like activating an item that is part of a stack. Presumably, you don't want to light the fuse on all your dynamites, so we should probably make it obvious which one is now lit so you can drop only that one.

Wildfires got really bad in Spokane and I got evacuated to a level 3 alert (go now). The fire got to about 1km of my airbnb and I was mentally prepared to lose what I had left behind. I was able to come back and all my stuff is fine. I'm still under a level-1 evacuation watch (be ready), that is: go-bag by the door. That adventure messed with my focus, but it got me to think about what is important in life: mechanical keyboard or water filter?

NO BLOCKERS!

2023-07-29

I was out camping in Yellowstone this week, so not much progress, but I feel greatly energized!

I added tile highlight to show where there is a default action for something. The default action is activated by a single tap: attack an enemy or talk to a friendly NPC.

JT Wright implemented ranged combat.

JT was really eager to push the improvement, so he pressed Enter as soon as he heard me say git reset --hard, before I had a chance to mention the name of the file to reset. He lost the first implementation of ranged combat, but he didn't let that get his morale down and he re-implement the whole thing right away. What a trooper! We only support ranged weapons that are their own ammunition (ex.: daggers) right now. Next stop, ranged weapons with separate ammunition supplies (ex.: bows, guns).

I paired with JT to make a lightning spell. The spell stuns, which is scary effective! We still need a sound effect for that one and there is a magical algebraic spell that we need to unlock to stretch the zapping bolt from the caster to victim.

NO BLOCKERS!

2023-07-15

I added a healing spell.

The visual effect for this one is a Godot particle system, no custom shader code. The sound effect is me singing. I pitch shifted several copies of the clip to simulate multiple voices before adding some reverb. Audacity has a reverb preset for "cathedral", which seems like an appropriate venue for a choir of angels to be singing in.

2023-07-10

Adding the Monte Carlo simulator went really well. With great foresight, I had written the main loop without assuming that one of the actors would be controlled by the player. I was therefore able to call that loop unchanged in the simulator. Furthermore, all the monsters were really happy to beleive that they would be unseen if there was no hero at all in the game (rather than the hero being out of perception range), which means that they just didn't do their animations in simulation mode. Sounds and a few minor animations were missing that perception gating, but that was loudly obvious and easy to fix.

Then it was just a matter of making the main loop restartable and to add a few more combat signals to make the simulation stats more detailed.

The performance turned out to be better than I was expecting, between 1000 and 2000 combat turns per second for most monsters. I tried the Godot profiler for the very first time to see if there was something obvious that could be improved.

The Godot profiler is interesting. The overhead is pretty small, about 20%, but there is unfortunately no call graph. The stats are aggregated per rendering frame and you can go back in time. So if there is one frame out of 20 that is slow to draw, you can click on the graph where the drawing time spikes and bam! You see the per-function time break down from back when this frame was drawing. So cool!

For my combat code, there are plenty of small gains to be made, but they would all require some non-trivial changes so I'm resisting the great urge to fiddle and not optimizing anything right now. Tuning the balance is more urgent than speeding up combat calculations.

A simulation run looks like this:

Starting a batch of 100 sims for AverageJoe, Rat, Rat2, Rat3...
Ran 100 sims in 1.26 seconds (421.64 turn/s)
Median encouter lasted 5 turns
Victory: 99.00%
  median gladiator health: 85.00%
Defeat: 0.00%
Draw: 1.00%
  median gladiator health: 100.00%
  median challengers health: 100.00%
AverageJoe's hit rate: 67.76%
Rat's hit rate: 76.07%
Rat2's hit rate: 72.85%
Rat3's hit rate: 78.28%

You can think of the "gladiator" as the hero, but it's controlled by the sim rather than by the player. It's a perfectly average human in this case and he's not great at hitting rats, but he kills them fast when that happens. That fits what I wanted to model: rats are weak, but small and hard to hit. (I don't know what happened during that sim that resulted in a draw.)

Then there is this one:

Starting a batch of 100 sims for AverageJoe, Ghost...
Ran 100 sims in 1.36 seconds (1221.98 turn/s)
Median encouter lasted 17 turns
Victory: 0.00%
Defeat: 100.00%
  median challengers health: 81.08%
Draw: 0.00%
AverageJoe's hit rate: 11.74%
Ghost's hit rate: 86.67%

Ghosts are meant to be elusive and hard to hit, but this is obviously too hard. Thank you simulator, now I know where to do some tuning!

Next stop, auto-tuning...

NO BLOCKERS!

2023-07-07

Revengate v0.9.0 is on Google Play (for some countries). This build has the magic from last week, silver weapons, and a second quest!

Building the second quest was a lot of fun. I had to rewire a few things to distinguish between a chapter victory from a game ending victory. The new set of dialogues is starting to give some personality to the NPCs, and the quest reward feels great! There were a few UI changes and usually I find all sorts of excuses to procrastinate when it comes to UI, but for those the motivation of seeing the new quest was strong enough to get me to do UI first thing in the morning!

Tuning the balance has been very time consuming. I don't mind playing the game, but doing it non-stop just to move a few numbers is too inefficient. It's time to bring back the Monte Carlo Simulator!

The old Python version of the game had a perfect separation between calculations and presentation, so it was really easy to make a simulator for it. With Godot, presentation is coupled more tightly, but I think that the work I did on perception and visibility should be enough to run combats faster than game-speed (monsters that you can't perceive move without animations).

In any case, getting that working is my priority for next week.

NO BLOCKERS!

2023-06-30

Revengate has magic!

This is pushing what GIFs can do, here's a better encoding of the demo.

I kept pushing back on this one, partly because I was not sure about the magic system (scroll all the way down), which is a bit of a fusion between the systems in Wheel of Time and American Gods with a steampunk twist. At some point, I figured that it's probably better to have lots of an OK magic system than just a tiny bit of a great one.

OK, I went a bit wild with the summoning spell. If you squint, you can probably guess how it works. The key is picking good looking decals to start with, then it's just a bit of pixel shifting with some colorization.

About half of the magic rules are implemented (the phantruch are bound to a vital assemblage and there is a channeling device to decrease your mana cost). Only monsters can do magic for now – I need a few more spells before I can make up my mind on how to expose the whole thing to the player.

2023-06-24

You can open and close doors. There are new "vibe" nodes, things you feel while walking around the dungeon.

Those are not used by the level generator yet; they will probably come in a late pass and be tied to active pre-fabs and to whichever monster cards were drawn for the level.

Added end-of-game stats: easy stuff stuff like the number of turns and the list of victims.

Added silver weapons: better to-hit against ethereal monsters.

async vs sanity

My brain almost melted trying to figure out why items sometimes didn't reappear after being stepped over. This was a really rare thing, maybe 1% of the times. Playing with the duration of the actor move animation and the item fadeout animation allowed me to repro about 25% of the time. I was happy about that and I added a bunch of print statements to see what was going on. Just like that, the problem was gone! The time it took for Godot to print was slowing things down enough to perfectly resynchronize everything.

Change of strategy: sprinkle asserts without printing anything in hope of catching the problem and getting a stack trace when the problem happens. It turns out that there can be quite some time between then the first and when the last handler for a signal are called.

When an actor is still in running their animations, they can end their turn and let other actors play. This increases the game pace and is more fun to watch. It order to keep the game consistent, however, that actor can't start their new turn until they are done with their animations. So one signal handler on Tween.finished decrements the active animations counter and then fires another signal to let the game loop know if the next turn can start for this actor. If the animation is a move, there is another handler on Tween.finished to do more internal cleanup.

So once in a blue moon, when the game loop sees that the actor is good to go, the next turn starts for that actor, the actor begins moving, and about half way through their move, the original move cleanup handler for the previous turn fires. Damn! That messes the internal state of the active move and the item on the floor suddenly thinks that someone is still stepping on it.

So moral of the story is that you can keep adding stuff to Tween.finished, but if you depend on a reliable timing, then all the important work has to be done in the first handler that you register.

2023-06-15

JT Wright made a really cool particle system for the dynamite.

You now perceive monsters only when you are close enough.

A direct line of sight allows you to perceive further, but it seems just fair that you'd be able to smell or hear monsters hiding just around a corner. There is a new potion of booze that will increase your regeneration for a little while but it mess with your perception (among other things). Items are always visible, this is a conscious design decision to torment the player with known potential rewards vs the unknown risk of getting beaten up.

All of this plus last week's progress seemed like a substantial enough increment so I pushed v0.8.0 to Google Play.

2023-06-10

Revengate has an overworld map.

While crossing a traboule, stairs and gates mostly move you towards the cardinal point where they are located, but the map is disabled underground to keep the experience slightly disorienting.

I'm now smarter with where stairs are placed by maximizing multiple Dijkstra minimums. Stairs used to be placed randomly and that makes a good level on average, but there were too many cases where up and down staircases would be right next to each others, allowing the player to completely bypass the exploration of that level.

Now after placing a cross-level connectors (stairs and gates), I measures all the distances from it using the Dijkstra algorithm. Each subsequent placement picks a cell where the minimums of previous Dijkstra metrics is maximal. This is probably easier to understand with a picture.

Here the gate on the right is placed first since it has a stronger constraint: it must lead to a location East of here. The Dijkstra metrics from it are in the first matrix. The stairs going down are easy to find: distance 41 at top left. We make another set of distance measurements starting from there.

Things start to be interesting when trying to place the upstairs. For each position on the board, we find the minimum from both Dijkstra matrices at that position. The optimal placement is where those minimums is the largest: dist=25. This is also in the top left, but quite the detour away from the downstairs. Ties are typically numerous, especially with large rectangular rooms. Those are resolved by picking a random maxi-min coordinate.

2023-06-03

I have pinch to zoom!

This was not as hard as I was expecting it to be and the improvement to the player experience is hard to overstate. My old zoom slider bar was either hard to hit or it took too much of the precious mobile screen real estate. It really paid of that I was scared by this change and that I sat down to flesh up the whole plan on paper before even looking at the code.

Godot does recognize some multi-touch gestures in the internal API that it uses to build the editor, but unfortunately none of that is exposed in GDScript. Godot exposes touch events with an index attribute that is unique to each finger and you need to keep track of previous events to decide for yourself if you are inside a gesture. When you receive a tap (InputEventScreenTouch) with index=2 (third finger on the screen), there is no information in GDScript to tell you if the other fingers are still on the screen nor where they were last time you saw them. Hopefully, you've been recording that back when those events fired. Another "interesting" quirk of the Godot API is that Buttons are blind to touch events. Buttons just don't see taps at all. You can enable mouse-emulation, but then you receive both a mouse event and a touch event every time a finger does something on the screen. The only way I found to take care of this was to detect the platform and ignore mouse events in the gesture recognizer on mobile. This is OK, but it means that a player who paired a bluetooth mouse with their tablet will be stuck to do all the gestures on the touch screen.

With something like Google Maps, zooming and panning is the same multi-touch gesture. If your fingers are moving but keep the same relative distance, then it's a pan, if their distance change but that the centroid of all fingers stays in the same spot, then it's a zoom, if both happen at the same time, no problem: zoom + pan. A multi-touch gesture is two or more fingers on the screen, and thinking too hard about how to track the motion of each fingers can easily melt your brain. I think that the key to stay sane is to only track the centroid of all the fingers and the average finger distance to the centroid.

So that's what I did and I almost shed a tear of joy when it just worked (ignoring the velocity tuning).

I added prefabs with a definition string telling me where and what. For example, adding a river (r) on the East side of a new game board E will be: Er. That should make it easy to define the Lyon surface in terms of its major features in a 10x10 matrix of prefab strings. Prefabs all act on a rectangle and they return the sub-rect that was not fabbed. Multiple passes of procgen can therefore easily avoid stepping on each others toes.

PyPI, the Python Package Index, recently deprecated GPG. Reading the really well written annoucement and it's associated links sent me a bit down a rabbit hole. In the end I could only conclude that it was not just me who it not smart enough to remember how to manage my GPG keys, it's that the whole thing sucks. The usability problems will be obvious to anyone who tried to do anything with GPG, but the crypto happens to be a problem as well. It has not been broken, but the security guarantees provided by GPG is incredibly weak compared to something more modern like Signal that will have one key pair per person you communicate with and that will change it every single message. A compromised GPG key would be the failure of the whole system while a compromised Signal key is no more than one compromised message.

Of course, Signal is not a solution for code signing, but I was really happy to discover that OpenSSH can do that part and that it's been integrated with git. It's a little weird that they call the ssh signing gpg.format, but the UX is perfectly transparent otherwise and both GitHub and GitLab will show those ssh-signed commits as "verified" like they do for gpg-signed commits.

Things get really cool when you add age to the mix. With age, you can take the public ssh key that GitHub and GitLab displays for my commits and encrypt a message for me with it. You have a really good sense that the key belongs to me since I signed all this code with it, so if you can't meet me in person for a key exchange, you still have a solid option for an initial contact.

My GPG key is expiring in 11 months and I don't think I will bother to extend it or create a new one after that.

My laptop battery died: it's all poofed up like a bag of chips. Thankfully I use a System 76 computer and all the service manuals are online. It was really easy to take it out while I'm waiting for the replacement and doing it myself does not void the warranty. I'm stuck staying plugged in the mean time.

2023-05-27

Revengate v0.7.0 is on Google Play. It features a new quest that is starting to show some steampunk vibes.

Levels are populated with monsters and items using card decks. Last week focused on teaching the deck generator about world locations so that the quest item and boss would be generated when you end up in the right part of town.

Now that the above is out of the way, I fell ready to tackle my nemesis: multi-touch gestures. Let's see if I can get pinch-to-zoom to work better that the magnification slider-bar.

2023-05-20

I moved to Lyon, France and I'll be here for another seven weeks.

Revengate is set in Lyon and I was at the point where I was designing the surface levels when it dawned on me that Google Maps was not the right way to immerse myself in the vibe of the city. So in what could be the greatest yak shaving of the decade, I packed my bags and came to see the traboules in person.

Unsurprisingly to many of you, this exploration probably won't have much influence on the level design: the low graphics style is meant to let your imagination run wild rather than depicting things in great details. The influence on the quests and stories, however, will be profound.

I had greatly underestimated how much mystery and intrigue was hiding around every corner of those narrow twisty streets. Secret societies, black magic, knights templar: they all had a well documented representation in the city for centuries. More on that in future updates once I start integrating those ideas in the storyline. For now, the real Jacquard loom completely blew my mind. This programmable machine will receive a steampunk twist earlier in story line with punch cards being commonly used to represent hidden messages in addition to weaving patterns.

I played with the tile atlas to depict a cleaner surface world and darker and older subterranean world. Most of my work during the past few weeks went into eating cheese, err, I mean allowing the procgen to know what level design to use depending on where you are in the world. I want to have parametric prefabs to decorate the obvious features like the two major rivers, but for now the transition between the two vibes is working well. A traboule dungeon correctly links two parts of the surface city.

2023-03-25

I added a maze generator.

It's reasonably tunable with various biases: should corridor be long and straight or short and twisty, how often do you see dead ends, is there more than one path between any two points? It also supports masking, which is another way to say that I can tell it to ignore arbitrary areas of the board.

I had a tunable generator in Python based on the Recursive Backtracker algorithm. For this one, I went with the Growing Tree algorithm. Growing Tree is a only slightly more complicated, but it enables the bias for more dead ends, which was impossible to control with the Recursive Backtracker. Since one is recursive and the other is iterative, I didn't review my Python code and just started typing in Godot. I was pleasantly surprised that it worked on the first try.

Even though I didn't look back at the Python code, writing it really helped me get a good intuition on which biases gave a good return on investment. It's weird, but there is a part of my mind now that thinks like a maze generator. When I look at a street map, I see how it would be like with different bias parameters.

Both algorithms and many more are covered in Jamis Buck's excellent book.

I finished reading Shadow of Self. What an ending!

2023-03-17

I added a dynamite item.

This was a big refactor since it's my first item with an expiration time. I standardized how things age and now I expose the same API for Strategies, Items, and Conditions. That allows me to age everything without knowing their type. I also disabled animations for things that are not on the active board, so before returning to a previously visited level, I can quickly refresh all the conditions and blow up any lit dynamite you left behind before showing you the level. Disabling animations is going to come very handy when I resurrect the Monte Carlo simulator.

I couldn't find a good sound effect for the explosion, so I ended up voice acting it.

Turns out Godot did not enable Android gestures in 4.0. What got enabled is a limited Kotlin API that is use by the Godot editor on Android. Long story short, I will have to implement gestures in GDScript. I'm saving pinching for later, but I was able to do the long tap in just a few lines and a Timer. I used it to implement a context menu. Feel free to force yourself to attack the quest NPC. He's got good loot.

2023-03-11

I started playing with shaders and I'm really happy with how the water surface and shockwave VFX came out.

Godot shaders are 95% GLSL. There is a tiny bit of syntax extension to help you tell Godot what kind of data your shader is expecting, which in turn lets the editor give you a fancy point-and-click UI to supply the shader data. Also, rather than having main() be the entry point for both the vertex and the fragment shader, they start with vertex() and fragment() respectively, which means that you can put them both in the same file. Nice! The editor does an excellent job of reloading the shader preview as you type – you don't even need to save – which makes experimentation very dynamic.

I finished implementing the parametric card deck rules and I can now compose them as draggable Godot nodes to specify what goes inside a dungeon. It works for both monsters and items. It does not have cross-cards dependencies yet, which is something I would like to add at some point: it just seems fair that you would not encounter ghosts until at least one silver weapon has been generated.

Multi-turn commands can now be cancelled.

2023-03-03

Revengate 0.5.0 is available on Google Play and for manual Android install. This version speeds up the game play by having more animation overlap. Rats are more interesting: they mostly follow walls and will generally leave you alone, but if you bother them, they can either attack back or run away.

I paired a bunch with JT Wright on the rats behavior. We started working on more interesting deck based generation for monsters and items to support more rules than the current "spawn cost", like floor limit and uniqueness. Most of it is still too rough for this release, but it should make it into 0.6.0.

Minor UI changes include messages flashing briefly over the game screen and an hour glass overlay letting you know when it's not your turn yet. There are now in-dialogue actions, so be sure to have a chat with the barber – he might have something for you.

This felt like a good streak since the last release. The only time I really started procrastinating was when I was trying to style narrations. I was able to recognize what was happening early so I dropped those from this release and went back to working on bot strategies, which is always a great source of joy. Having blockers for news sites really helped me to see that I was procrastinating more than usual. Blockers are easy to disable, but disabling them is just hard enough to bring a sense of realization.

Godot 4.0 came out. There's nothing in this release that I was not already using from the latest Release Candidate, but it's really good to see that we finally hit that milestone. I'm already extensively leveraging the new TileMap editor and I'm looking forward to start playing with the new 2D lights in 4.0.

I finished reading Mortal Engines. The air battles were quite good! I started reading Shadow of Self, the second book in the steampunk cycle of Sanderson's Mistborn series. It's really good! There's a deeper detective twist to the story and the two sidekicks get their own perspective with a better character development. The whole thing is satisfyingly wrapped in metal-base magic, with a broader showcase of abilities. Nice!

2023-02-24

Monsters are now generated using a "probabilistic deck". Rather than storing multiple copies of the monster cards, we only store the number of copies, which can be a float (you need >=1 for a draw). This is faster and more compact. It could be tuned a tiny bit to keep track of cumulative weights, then a draw can be done in O(log n).

I managed to wrap my head around how the Dialogue Manager plugin tracks the conversation flow and got conversation options to work. Actors also remember where you left off and will potentially start right there if you talk to them again. Conversation actions, like giving someone a potion of regeneration work!

Rats now have a strong bias towards traveling by hugging the walls. This was consuming me all week: how to do it cheaply without completely blowing up my A* implementation. I played with different metrics on paper. I found one that seemed promising, implemented it, then it just worked! It's a slight bias against diagonals and a bigger penalty for going somewhere with fewer walls, only counting 4 of the 8 adjacent cells. It turns out that Manhattan distance is a really good way to estimate the diagonals bias, so I can use that as my heuristic in A* and keep most of the A* bad paths pruning properties.

I worked with someone to add a FlightOrFight strategy on rats. It's pretty good already, but it needs a bit of tuning before players can start exploiting rats as decoys.

I revisited my nemesis: animations overlap. I stopped waiting for animations to be done before moving the turn counter. This really speeds up the game play! There are still a few cases that explicitly wait for anims, like an actor has to finish moving before they can take their next turn action. Strategy.act() returns a bool rather than an animation; the few places that need to sync on the anims have to poke the Actor state machine. There is room for more tuning, but it feels really good right now and I don't plan on playing more with it for a while.

Godot released 4.0 RC4 (then RC5 two days later). This is a big deal for Revengate because many of the Android gestures are now recognized by the engine. I should be able to replace the stupid zoom slider bar with pinching soon.

I played a few games of One Deck Dungeon, a table top roguelike. It's hard! The solo mode is pretty fun. It's also probably longer that the advertised 30 mins for most players. I don't think that there are any mechanics that would port to Revengate, unfortunately. I like the anti-grinding design: any action costs time, which brings you closed to a deeper dungeon floor with more challenging encounters. This is a lot more fun than hunger.

2023-02-17

Revengate 0.4.0 went out to Google Play (also available for manual side-loading). This release includes the monsters stat page, monsters who remember what happened to them, non-aggressive rats, dialogues, and a skills system that is not exposed to the players yet, but which already reduces the number of misses during the typical encounter.

The Godot Dialogue Manager plug-in is excellent! It makes it really fun to write the dialogues and it has all sorts of plug points to let the conversations change the global game state. Integrating it is some work, however, and I have yet to take advantage of all the features.

Actors remember stuff that happened to them. This allows more interesting monster strategies, like self-defense (done) or fight-or-flight (todo).

The 3-stage skill system can be an excellent basis for character progression and it would map well into a skill tree (ex.: get Advanced with Blades before you can unlock Rapiers OR Sabers). For now it only decouples agility and to-hit, which results in fewer misses during the typical encounters. I much prefer those shorter and more eventful encounters.

I could not find spooky enough sound effects for the ghosts, so I started recording my own. I'm quite happy with the results! I used Audacity with very minimal manipulation of the voice acted samples. With a dozen takes in a row, one is bound to be good.

I worked with a contributor to revamp the behavior of rats. Mostly, they don't care about you unless you bug them. There is a lot more to come, like the difference between them spotting you from a distance vs being startled when you surprise them.

I upgraded to Godot 4.0-RC2. It's mostly about stability and polish now. The engine is more enjoyable every week.

Since the release, I also added multi-attacks. The result is brutally effective. Some tuning will be needed with this one.

I finished reading Allow of Law: so good! The book is completely action packed with a very interesting and very steampunk magic system (the Wild West flavor of Steampunk).

I started reading Mortal Engines. The steampunk concept is pushed much deeper in Mortal Engines. It's yet another steampunk story that is happening in London, but this is a much much different take on London. It's generally enjoyable, but nowhere close to be as good at Allow of Law. I don't think I will go through the whole series.

2023-02-03

We have a bestiary!

bestiary

I paired with a new contributor to implement monster spawning based on a per-level budget and spawning decks. This keeps the per-level difficulty equal whether there are lots of easy monsters or just a handful of really hard ones. That is of course assuming that we can get the difficulty metric right, which is probably not true right now, but I'm confident that we can get there with the Monte Carlo simulator.

We currently have spawning decks with duplicate "cards" of easy monsters. I want to reimplement those with single instances and the probability value instead. Those will come in handy for many other aspects of procgen.

2023-01-26

Lots of minor loose ends to tie this week for the v0.3.0 release. This release features an actual quest that felt surprisingly well balanced despite most of the key numbers coming from wild guesses. It's pretty hard the first few times, then you figure which items are worth picking and it becomes probably winnable 2/3 of the times.

The highlight of the week was implementing healing, both from innate regeneration and from items. Regular items won't heal you more than your full health level, but magical one can go above your max (ahem quest hint ahem). The items design fully embraces the Godot node composition paradigm and quest designers can drag and drop a bunch of effects on their new fancy items.

Next week, the work on the real fun stuff starts: bestiary and/or dialogues.

2023-01-21

This was another good week. The highlight is definitely that after installing Godot 4-beta13, I discovered that deploying to Android automatically starts the remote debugger, which makes it ridiculously easy to inspect and manipulate the scene tree of the app running on the phone.

I don't know for sure if it was already there, but I know I will take advantage of it a whole lot more from now on.

I added effects and conditions for damage over time, like poison and being on fire.

I added cheat codes, like teleporting and printing the internal data for a given location, but with the newly discovered power of the Godot remote inspector, those won't be as useful as I originally thought they would be.

I added victory conditions, so you can finally win the game rather than infinitely bashing monsters. I have a few things to tidy up, then I will probably push a v0.3 build to Google Play next week.

I started reading The Alloy of Law by Brian Sanderson. That's is a really interesting take on steampunk magic. As expected from Sanderson, the magic system is very elaborate with obvious limitations. Without spoiling it, some people are born with the ability to influence metals. In medieval time, that was probably cool but not that useful, in the middle of the industrial revolution, that's obviously game changer.

2023-01-13

Items: items fade away when stepped over. They can be picked up and dropped. I'm still working on the inventory dialogue. I have something working, but it's kind of clumsy.

items-fadeout.gif

Godot supports buttons in tree-view records and it does a good job at dispatching input events to them, but it only allows sprite buttons, so that's a whole new asset class to maintain. I suspect that it's possible to extract the textures from regular buttons to make the whole thing follow the lovingly crafted UI theme, but I have yet to unlock that spell.

Items don't do anything yet, but I have a fairly good idea on how to do that part. It will be very similar to what I had in the Python implementation of Revengate.

Next week: victory conditions; maybe: items for progressions and damage over time.

2023-01-05

Revengate 0.2.0 is ready for play testing on Android. That's my first public build of the Godot reimplementation! It doesn't do much, but you can beat up monsters and go infinitely deep in the dungeon. Google Play will probably take a few more days to show the latest version, but the manual APK install is ready for action.

What level did you reach?

As I was making zooming and panning more mobile friendly, I got really confused by how events were processed. It just didn't seem to match the way it was described in the doc, so I set up a few experiments with lots of logging to really understand how Godot handle user input. Here's what I found.

All input events propagate through 3 main phases:

  1. Node_input() in reverse traversal order for all nodes in the scene;
  2. Control._gui_input() for the control (widget) where the click was or for the control with focus in the case of keyboard events;
  3. Node._unhandled_input() in reverse traversal order for all nodes in the scene.

Here, "reverse traversal order" means that we start at the bottom of the screen tree as rendered in the editor and go up one line at a time.

Disabled controls still get _gui_input() and _unhandled_input(). Invisible controls pass _gui_input() to their parent, they still receive _unhandled_input(). The SubViewportContainer receives the _gui_input() for the Viewport.

Calling either Viewport.set_input_as_handled() or Control.accept_event() has the same effect: it immediately stops the propagation of an input event. They can be called from any of the _*input() methods and they do not need a reference to the event. Accepting in the _gui_input() of a control will block the built-in behavior of that control. For example, calling accept_event() in MyButton._gui_input() will prevent the pressed signal from being emitted.

The z_index property of a node can make it to show on top of other graphical elements, but it has no effect on which control will receive the _gui_input. Only the order in the scene tree can change that.

I also ran into an interesting problem: SubViewports do no remap user-defined InputEvents. That forced me to understand Transform2D, a 2x3 matrix that is a property of all Godot Node2D objects. A position vector can be multiplied with it to translate, scale, and rotate it, but since it's the transform to render a Node2D, inverting the matrix is often required depending on which coordinate system you want to remap to.

While pulling my hair (figuratively) to figure in what order I had to apply the above transform, I adopted following convention in all my logging messages:

  • pixel coordinates are noted "(x, y)"
  • tile coordinates are noted "[x:y]"

This really helped!

I got rid of spacial audio for now, but I like the concept. I will definitely revisit it later.

I started reading Leviathan Scott Westerfeld and Keith Thompson. Now that's one damn good steampunk novel! The main premise of steampunk is that some kind of technological advance came in early during the Victorian era, but everything else about social dynamics is pretty much the same. In Leviathan, you are greeted by bioengineering and Gundam-style mechas during the 19th century. Wow!

Older Updates