Skip to content

Quests and map actions

Quests connect game events to a supported list of actions. A simple quest can reveal your map battle at session startup. The current interface is a typed compatibility format: it does not take arbitrary Lua quest callbacks. Put custom combat calculations in behavior functions instead.

Quest actions use the actions array and a type discriminator shown below. Move timelines are a separate contract: use the move short form when authoring combat animation actions.

Create a quest that responds to selected game events.

Signature: sf2.quests.register(definition)

Requires: content.register, plus dependencies for referenced content.

When: Entrypoint, after the battle/fight/item definitions used by the quest.

Returns: A quest handle.

Field Type Default Meaning
id String Required Stable local quest ID.
priority Integer 0 Native quest ordering priority.
unresumable Boolean false Native non-resumable quest flag.
allow_doubles Boolean false Allow duplicate active instances under native quest rules.
place String "map" "map", "fight", or "dojo".
groups, marks String arrays Empty Native quest grouping/mark metadata.
events Event-name array Required At least one supported event.
conditions Condition array Empty Conditions that must hold.
actions Action array Required Supported actions to perform.
-- battle is the handle created by sf2.battles.register.
sf2.quests.register {
id = "reveal_training", place = "map", events = { "session" },
actions = {
{ type = "show_battle", battle = battle, locked = false },
{ type = "map_focus", battle = battle },
},
}

Use dense arrays starting at index 1; do not leave holes. Unknown fields and unsupported action/condition names are errors. Registration alone does not run the quest immediately; its event and conditions control activation.

Event Context
session, activate Session initialization or quest activation.
fight_enter, fight_end Ordinary fight entry/result.
raid_fight_enter, raid_fight_end Registered offline raid fight entry/result.
raid_enter, raid_end Raid attempt entry or sequence completion.
reset_mode Mode reset.
raid_map_enter, raid_floor_changed, show_raid_loot Raid map/progress/loot events.
level_up, got_item, set_item_acquired Player progression and acquisition.
purchase, delivery, timer_end Purchase, delivery, or timer completion.
enchantment, activate_perk, deactivate_perk Enchantment/perk activity.
dialog, map_button, scene_loaded, shop_enter UI and scene events.

These names select existing host events; they do not create new gameplay systems. Filter a fight-end quest to your own fight so it does not run after every battle.

A comparison is { op = "eq", left = ..., right = ... }. Operators are eq, gt, gte, lt, and lte. Optional not = true negates the result. Because not is a Lua keyword, write the key as ["not"] = true in actual Lua code.

Group conditions with { op = "all", conditions = { ... } } or op = "any". A group also accepts ["not"]; do not add left/right to a group.

Operands may be string, number, or boolean literals, or these tables:

Operand Fields Meaning
{ kind = "variable", name = "..." } Required string name Native user variable.
{ kind = "event_fight" } None Fight associated with the current event.
{ kind = "fight_result" } None Native fight result value.
{ kind = "current_battle" } None Current battle identity.
{ kind = "fight_wins", fight = handle } Fight handle Saved win count for that fight.
{ kind = "fight_id", fight = handle } Fight handle Identity of a specific registered fight.
-- As the conditions field of a quest responding to fight_end:
conditions = {
{
op = "eq",
left = { kind = "event_fight" },
right = { kind = "fight_id", fight = training_fight },
},
}

sf2.state fields are a separate typed save API; a quest variable operand does not automatically read them. Use this compatibility format only for its supported host semantics, not as a general arithmetic or programming language.

type Additional fields Effect
show_battle Required battle handle; locked=false Reveal a map entry with a lock state.
toggle_battle Required battle; visible=true Show or hide an entry.
map_focus Required battle Focus its map entry.
fight Required fight handle Start that fight.
current_fight None Start the currently selected fight.
eclipse enabled=true Toggle Eclipse mode.
update_eclipse_battles None Run the game’s full Eclipse battle update (see below).
give_item Required item handle Grant an item through the host.
set_variable Required strings name, value Set a native user variable.
show_map_button Required id, image, x, y; optional anchor_min_x=0.5, anchor_max_x=0.5, show_type="both" Add a native map button when this quest runs.
hide_map_button Required id Remove this mod’s map button <mod-id>.<id> from the map and the saved profile. Does nothing if it is absent.
dialog title="", image="", required lines; optional button Display a dialog.
story Required lines Display a story screen.

update_eclipse_battles runs the same update the game’s own Eclipse quests use at session start (UpdateEclipseBattles with its default INIT trigger). For every story zone, each normal battle that names an Eclipse counterpart (eclipse_toggle_name in battle registration) is checked:

  • The pair is skipped until the normal battle’s last fight has been won. Its lock state is not considered.
  • Before the final battle of Act VI is won, plain boss battles pair and intermission bosses do not; afterwards it is the other way round.
  • The normal entry is hidden in Eclipse mode and shown in normal mode.
  • The counterpart is added to the saved roster (or rewritten if present): shown in Eclipse mode, hidden otherwise, unlocked, with its replay count reset to the win count of its own last fight.

The action has no fields. The game’s own mode switch (TOGGLE) and post-fight (FIGHT) updates are driven by the vanilla Eclipse quests and are not available as Lua fields. Use the action after you reveal a pair whose normal battle is already complete, for example from a session quest.

-- Re-sync Eclipse pairs once per session after this mod revealed its own battles.
sf2.quests.register {
id = "sync_eclipse_pairs", place = "map", events = { "session" },
actions = { { type = "update_eclipse_battles" } },
}

Older Eclipse builds ran their own replacement of this update after every fight, session start and scene load. The game now follows the original quests: automatic mode switches and post-fight updates start once the player has finished the Eclipse replay tutorial (or won the Act VI final battle).

Dialog/story lines are strings or tables { text, button?, frames? }. text is required; button defaults to "" and frames to 0. Presentation strings may be supported localization references.

For show_map_button, id is 1–64 lowercase ASCII letters, digits, _ or -. The native button name and the map_button notification’s button field are <mod-id>.<id>, so mods can distinguish their clicks. image is either a typed sprite handle from sf2.assets.sprite (recommended for mod artwork) or an installed native Textures/ path. A bare qualified asset ID string is not accepted; obtain its handle during loading. Coordinates are finite map coordinates. Anchors must be finite values between 0 and 1, with anchor_min_x <= anchor_max_x. show_type accepts both, story or raid. Place the quest on map and give it events = { "session" } for a button shown at session start. React to a click with sf2.story.on("map_button", ...). If a profile already contains a button with the same name, running the action updates its saved image, placement, anchors and display mode. This lets a mod move a button in an update without leaving the old position in existing saves. The action supplies the native button only; the click callback decides what to do. For example:

sf2.quests.register {
id = "arena_button", place = "map", events = { "session" },
actions = { { type = "show_map_button", id = "arena",
image = sf2.assets.sprite("sprites/arena_button"), x = -400, y = -200 } },
}
sf2.story.on("map_button", function(event)
if event.button == sf2.mod.id .. ".arena" then
-- Open an owned UI view here (requires ui.create).
end
end)

Map buttons are saved in the profile, so they stay after a mod stops showing them. To retire one, for example after moving the feature into the dojo with sf2.ui.dojo_button, run hide_map_button with the same id at session start:

sf2.quests.register {
id = "retire_arena_button", place = "map", events = { "session" },
actions = { { type = "hide_map_button", id = "arena" } },
}

A dialog’s optional button is { text = "...", color = "Beige", actions = { ... } }: text and actions are required, while color defaults to "Beige". Button actions use the same supported action shapes.

-- As an action in a quest:
{
type = "dialog",
title = "my.mod:localization/battle.title",
lines = { "my.mod:localization/battle.introduction" },
button = {
text = "my.mod:localization/battle.begin",
actions = { { type = "fight", fight = training_fight } },
},
}

Use either a fight reward or a quest item grant for an intended single reward; using both awards twice.

Signature: sf2.quests.suppress { target = "namespace:quests/id" }

Returns: Nothing.

When: During registration, before the mod entrypoint returns. Changes take effect through Apply & Restart, before saved quests resume.

Requires: content.patch. The target must already be registered in your mod or an explicitly declared dependency. Declare core when targeting base quests.

Suppresses execution while preserving the native definition and saved progress. It covers event dispatch, explicit activation, direct Run/Foreach children and saved resume. It does not undo actions that ran earlier or mark the quest complete. Disabling the suppressing mod and restarting restores eligibility; normal quest conditions and completion state still apply. This is whole-quest suppression, not editing individual actions or replacing the quest’s behavior.

Core targets use core:quests/<source-file>/<quest-name> in lowercase, including .xml and the source path relative to the gameplay XML root. For example, core:quests/quest_extensions/energy.xml/energychecker. Includes retain their original source identity. Identical names in different source files are distinct; repeated same-name records in one source are suppressed together. Mod-owned quests use their ordinary qualified ID. The operation never replaces an XML file.

local sf2 = require("sf2")
-- Suppress an already registered quest belonging to a declared story dependency.
-- Replace the target with that dependency's actual quest ID.
sf2.quests.suppress { target = "my.story:quests/old_introduction" }

Unknown targets, undeclared dependencies, extra fields, duplicate requests and conflicting requests by two mods fail registration. Even dependency order does not silently override another suppression request. Suppression records participate in content fingerprints. Shared prices, currencies and economic formulas remain outside this operation.

Managed routing and Lua registration are tested, including recovered Run/Foreach entry and saved-resume methods with host services stubbed. Saved lookup matches both the quest name and its saved loading file, so a same-named quest in another loaded file is not selected. Full-game interruption/resume and native presentation acceptance remain separate checks.