With the help of ZeroKnight from the factorio forums, I managed to figure this out.
There is not currently a way to disable a custom achievement type directly via script, but you can replicate the behavior by using one of the built-in types, either DontBuildEntityAchievement which is simpler or DontUseEntityInEnergyProductionAchievement. These achievement types automatically handle updating the achievement UI when conditions can no longer be met. They also disallow earning the achievement when adding the achievement mod to an existing game (so players must start a new game).
There are four parts:
1. Define the achievement
This part is straightforward. As with adding any type of achievement, extend the data table with the new achievement settings:
data:extend{
{
type = "dont-build-entity-achievement",
name = "dont-die",
order = "n",
icon = "__(Mod Name)__/graphics/achievement-dont-die.png",
icon_size = 128,
allowed_without_fight = false,
dont_build = {"grave-marker"},
amount = 0
},
}
It appears that the "amount" value is inclusive. When I had it set to 1, I had to spawn two of my grave markers before the achievement would fail. Setting it to 0 fails the achievement after a single failure.
Note: Once you unlock an achievement, it is permanently unlocked for that character (stored in the player's data). While testing, I intentionally used different achievement names so I could attempt to earn the achievement normally once I was finished. Achievements that are no longer defined (like "dont-die-test") don't show up in the achievement list, so this won't result in a bunch of clutter.
2. Define the entity to fail the achievement
The next step is to define an entity prototype that when built will fail the achievement. I used SimpleEntityWithOwner with a 128x128 transparent sprite:
data:extend{
{
type = "simple-entity-with-owner",
name = "grave-marker",
icon = "__(Mod Name)__/graphics/empty.png",
icon_size = 128,
flags = { "placeable-player", "not-on-map", "hidden",
"not-selectable-in-game", "not-upgradable",
"not-deconstructable", "not-blueprintable", "no-copy-paste" },
indestructible = true,
picture =
{
filename = "__(Mod Name)__/graphics/empty.png",
priority = "extra-high",
width = 32,
height = 32,
shift = {0.0, 0.0}
},
collision_box = {{0, 0}, {0, 0}},
raise_built = true,
build_sound = { filename = "__base__/sound/wooden-chest-close.ogg", volume = 0 }
},
}
The build sound override was to prevent the standard entity placement sound from triggering when the entity was "built". Otherwise, it sounded like someone was dropping an inserter.
3. Define the item recipe to place the entity
The script function I use to place the entity relies on the entity having a matching item recipe (else the mod will prevent the game from launching). But I didn't want the player to stumble across this in the recipe book, so I made it a hidden item:
data:extend{
{
type = "item",
name = "grave-marker",
icon = "__(Mod Name)__/graphics/empty.png",
icon_size = 128,
flags = {"hidden", "only-in-cursor" },
place_result = "grave-marker",
stack_size = 1
}
}
4. Trigger entity placement when the failure condition has been met
This was the tricky part. My first attempt tried to use LuaSurface.create_entity(), but this does not fire all the build events even with the "fire build events" flag, so it would create my failure entity but not actually fail the achievement.
ZeroKnight pointed me towards LuaPlayer.build_from_cursor() instead. This builds the item in the player's cursor stack, which can be set with LuaItemStack.set_stack().
So in my control.lua I have:
script.on_event(defines.events.on_player_died, function(event)
local player = game.players[event.player_index]
player.cursor_stack.clear()
player.cursor_stack.set_stack("grave-marker")
player.build_from_cursor({
position = {player.position.x, player.position.y},
})
end
Now when a player dies, it drops an invisible grave marker and fails the "Don't Die" achievement.