3

I'm looking at adding some achievements to one of my mods and have only been able to find limited documentation. From what I can tell, achievements must be based off of one of the achievement prototypes. But I'd like to make something a little more complicated.

For example, for a multiplayer mod, I'd like to have an achievement for launching a rocket without dying (as in multiplayer, if you die, you respawn after a short delay). I can achieve the rocket part with a FinishTheGameAchievement, but tracking deaths would have to be done through scripting.

I know scripts are able to unlock achievements by calling player.unlock_achievement(name) but there doesn't appear to be an equivalent lock or disable call. Nor are there very many properties on the LuaAchievementPrototype class.

Is there a standard way that a script can prevent an achievement from unlocking? Or do I have to fake it with something messy like a DontBuildEntityAchievement with an invisible entity that spawns on player death?

  • Preventing an achievement to unlock has a single name : miss. A few achievements are missable in Factorio : non-peaceful (steam all the way, bulletstorm), timed (on a track like a pro), and counted (lazy bastard). When you magnet them you'll see them change when you cannot unlock them anymore. You should look that way. – Goufalite Sep 16 at 17:14
  • 1
    Some of those are because of the achievement type. E.g. timed is the FinishTheGameAchievement which supports a timer. Steam all the way is a DontBuildEntityAchievement. I can do similar stuff if I limit myself to the specific missable condition for each type, but in this case I'm trying to find a way to add new conditions through scripting that aren't supported by the prototypes by having the script "miss" them. – Troyen Sep 17 at 7:16
0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.