Dev Log: 2026-09-07¶
Two different complaints, from two opposite kinds of player, both pointing at the same decision: things that should have been separate switches were bolted together.
The feedback, from both ends¶
The action-focused players have been telling me the cutscenes get in the way. Every stage opens with a scene, and the only way past it is the skip button, stage after stage after stage. There was no "I have seen this, stop showing me story" setting, because story playback was not a setting. It was tied to whether you had seen a given scene before, and nothing else.
The players who care about difficulty have been telling me the opposite thing from the other side. Guided tutorials, the step-by-step coaching that walks you through your first encounter with each mechanic, only ran on Easy. So a player who wanted to start on Normal had to give up the tutorials to do it. "Play at the difficulty I want" and "learn the game properly" were mutually exclusive, and that is a bad choice to force on someone in their first hour.
These read like unrelated complaints. They are the same complaint. In both cases a thing the player would reasonably want to control on its own was bundled into something larger, so anyone who wanted most of the bundle but not all of it was stuck.
Cutscenes: from "first play" to a switch¶
The old model: each scene has an id, the game keeps a set of ids you have seen, and a scene plays if its id is not in the set. Clean, and completely outside the player's control.
The new model is a Cutscenes toggle, sitting on the world map next to the menu buttons and mirrored in Options. On means story scenes play. Off means they do not. The single decision point looks like this:
func _play_cutscene_slot(stage_num: int, slot: String) -> void:
...
if not should_play_cutscene(ConfigManager.cutscenes_enabled,
GameManager.has_seen_cutscene(cutscene_id), GameManager.playtest_mode):
GameManager.mark_seen_cutscene(cutscene_id)
return
await _play_cutscene_data(cs)
GameManager.mark_seen_cutscene(cutscene_id)
Two details in there matter. The first: when the toggle is on, should_play_cutscene returns true whether or not you have seen the scene. If you leave cutscenes on, you want the story every time you enter a stage, including replays. The seen-set no longer suppresses anything for a normal player.
The second: when the toggle is off, the scene is still marked seen before the early return. That way, if you turn cutscenes off for a while and then turn them back on ten stages later, the game does not suddenly dump every scene you skipped in the meantime. Off means skipped, not deferred. The scenes you skipped stay available in the Cutscenes and Key Visuals gallery, which is the right place to catch up on story you passed over.
The opening cinematic is not a stage beat¶
I shipped "cutscenes replay every time when the toggle is on" and within an hour hit the case that breaks it. Restart Stage 1, and you sit through the entire opening catastrophe cinematic again before you can move.
That scene is not a stage beat. It is the one-time cold open, the thing that happens before Erika wakes up, and it should play once per save and never again. Per-stage story scenes are fair to replay if you have asked for that. The opener is not. So it keeps a hard seen-gate that the toggle cannot re-arm:
if not resuming and not BuildConfig.is_reddit and stage_num == 1 \
and GameManager.current_chapter == 1 \
and not GameManager.has_seen_cutscene("intro_warning"):
The resuming flag in that condition is the other half. Entry cutscenes, the opener and the two pre-fight scenes, are beats for arriving at a stage fresh from the world map. They should not fire when you are dropped back into a stage you were already in. That happens three ways: resuming a stage you quit mid-mission, restarting a stage, and resetting a single sector. All three now pass resuming = true and skip straight to the fight, with the stage restoring its saved state underneath.
Tutorials: unbundled from Easy¶
Easy mode in this game does a lot of separate things at once. It thins out the enemy rosters. It refills your limit gauge every sector. It applies stat overrides. It gives you the undo button. And it runs the guided tutorials. That is five distinct kinds of help wearing one label.
The guided tutorials did not need to be in that bundle. The config flag for them already existed, invasive_tutorials_enabled, defaulting on. It just had no interface and was sitting behind an and difficulty == Easy check. Removing that check was most of the work:
# before
const NORMAL_DIFFICULTY := 0
if GameManager.difficulty != NORMAL_DIFFICULTY:
return false
# after
const MAX_TUTORIAL_DIFFICULTY := 1 # Normal
if GameManager.difficulty > MAX_TUTORIAL_DIFFICULTY:
return false
Guided tutorials now run on Easy and Normal, controlled by a Guided tutorials toggle that lives next to the cutscene one. Hard and above still never gets them: those scripts are written against Easy's pacing, and I am not confident enough that they hold up against a full roster to turn them loose there yet.
The combat aids stay tied to Easy, because that is the part that actually changes how hard the game is. What changed is that "I want coaching" and "I want the game to go easy on me" are now two questions instead of one.
The prompt that appears when you switch from Easy to Normal used to say, among other things, that you were giving up tutorials. That is no longer true, so it now says the tutorials keep playing, they just will not play out the same way without Easy's other overrides around them, and the undo button and combat aids do not carry over.
Reddit-lite: cutscenes off, tutorials on¶
The Reddit version of the game has no cutscenes at all. So it gets the guided tutorials toggle, since its Easy mode still runs them, but not the cutscene toggle, which would control nothing. One if not BuildConfig.is_reddit around the cutscene row in each of the two places the toggles appear, and the Reddit build shows exactly one switch where the full build shows two.
Bug fixes¶
- Startup crash on some Macs from the wrong renderer. The project had never explicitly set a rendering backend, so it defaulted to Forward Plus, which runs on Vulkan. On this machine, and I suspect others with the same GPU class, that path hits a MoltenVK compute shader compile failure at boot:
AIR builtin function was called but no definition was found, followed by a wall of null pipeline errors. This is a 2D pixel-art game. It was always meant to run on the Compatibility renderer, which uses OpenGL and is far more forgiving. Now it says so:
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
Booting with that pin goes from an error wall to a clean OpenGL API 4.1 - Compatibility line and nothing else.
- The Android device simulator forced fullscreen. The desktop script that emulates a phone screen for layout work stopped honoring its window size, because the
fullscreendefault flipped to true a while back and the simulator's autoload runs after the config that applies it. It now recognizes the--androidlaunch flag and stays windowed.
Miscellaneous¶
- Added tests for the cutscene decision function across the toggle and playtest cases, the resume and restart suppression, the tutorial difficulty gate on Normal, and the Reddit-lite toggle split.
- GDScript test suite: 1195 of 1198 passing (3 pending: sprite assets unavailable in the headless test environment, not failures).
- Python solver test suite: 595 passing (17 skipped). None of this touched combat, movement, drones, or progression, so the solver mirror needed no changes.
- Build 0.1.7.95 goes out to Steam, Google Play, and itch.io.
The pattern in both halves is the same, and it is one I keep having to relearn. A setting that bundles several kinds of help is quick to build and slow to show its cost. It reads fine in the design doc, where the bundle has a tidy name like "Easy mode" or "first-time story." It only falls apart in contact with a real player who wants most of the bundle and not all of it, and by then the fix is not a cleverer default, it is taking the bundle apart into the switches it should have been.