A game built from mods in Godot 4: switch them on, off, or delete the folder without breaking anything
Gotic is 31 mods you toggle from a menu and remove by deleting their folder. Keeping the game working was easy; keeping the test suite working was not.

In Gotic there is no "main game" plus extras. There's an island, a character who walks, swims and jumps, and an inventory. Everything else is a mod: the villages, the broom, fishing, the farm, day and night, the photo camera. There are 31 today. On the title screen, MODS lists each one with its switch, a description and what it depends on, and you can also change them mid-game.
There's a blunter way to remove a mod: delete its folder. The game still has to start, and the save must not lose what that mod had achieved.
How it's built
Every folder in modulos/ with a modulo.gd is a mod, discovered at startup:
static func available_modules(dir := "res://modulos") -> Array[String]:
var result: Array[String] = []
if not DirAccess.dir_exists_absolute(dir):
return result
for folder in DirAccess.get_directories_at(dir):
if ResourceLoader.exists("%s/%s/modulo.gd" % [dir, folder]):
result.append(folder)
result.sort()
return result
It asks ResourceLoader rather than FileAccess because inside the APK scripts are compiled (a .gdc plus a .remap) and FileAccess wouldn't see modulo.gd.
A mod is a node with four functions: setup(world), save_data(), load_data(data) and teardown(). It never touches main.gd; it uses the game's hooks (keys, touch buttons, map markers, USAR (use), bonuses, inventory). Dependencies are plain text, REQUIRES = ["aldea"], never a preload from another folder, so removing one doesn't cause parse errors in the rest.
Mods don't know about each other either: cooking asks every active mod whether it has ingredients(), so fishing and farming feed the kitchen without knowing it exists. When a mod is switched off, its saved data is kept aside, even if its folder is gone: bring it back and its records return.

What went wrong
Freeing the node isn't enough. The broom rides on the player's back, the flower crown sits on their head and the buttons live in the HUD: none of that hangs from the mod's node, so none of it leaves with it. teardown() has to remove all of it before anything is freed.
Modular in the game wasn't modular in the tests. Deleting a mod's folder worked while playing, but tests/run.sh failed: shared tests named specific mods (main.modules["carrera"]) and the race used the broom as "something that flies". The fix:
- Shared tests walk through whatever mods exist without naming any. One switches each mod on and off and compares a "footprint" of the world (interactions, markers, buttons, tree-free clearings, nodes attached to the player and the HUD): re-enabling must not duplicate anything and disabling must leave no trace. When one of the broom's cleanup steps was removed on purpose, that test caught it.
- Each mod's own tests live in its folder (
pruebas.gd). If a dependency is missing, those tests are skipped with a warning. - Dependencies are tested with three fake mods.
- And it was checked for real, by moving folders out: with the seven mods of that time, 583 checks; with none, 331; without each one, still
PRUEBAS OK.
How the agents work
Almost all the code is written by AI agents (Claude) following rules kept in the repository: a plan before starting (PLAN.md: tasks, decisions and what will not be done), one commit per task explaining why, and tests/run.sh ending in PRUEBAS OK before every commit. Every bug gets a new test that must be seen failing with the fix removed. When one of those mutations survives, a test is missing… or there's code that does nothing: removing a line from new_game() broke nothing because it was redundant, so it went.
Folders also split the work. When two Claude sessions ran at once, they shared the temp folder and Godot's user data, and one session's tests trampled the other's test saves. Now each gets its own worktree and XDG_DATA_HOME, they divide the work by mod folder and git add always takes explicit paths.
Still unverified
On a phone (a Samsung S24 Ultra) the title screen runs at 60 FPS, and MODS, "back" and JUGAR (play) responded to taps simulated through adb. But I haven't toggled a single mod on the phone, since that would have changed the real save, and I haven't tried it with actual fingers.