AI Agent Guide: Build Unreal Engine Combat with Claude Code, Codex or Cursor

Documentation Unreal Engine AI Tutorial

An Unreal Engine 5 combat plugin made for AI coding agents: hand Claude Code, Codex or Cursor the guide the plugin ships, and it builds a souls-like enemy from your own mesh and animations in your open editor, with no C++.


The plugin ships a guide written for an AI coding agent; the agent itself is not included, so use the one you already have. Hand it the guide with the editor open, and it builds an enemy from your own mesh and animations, then a weapon, combos, reactions and a boss.
The guide is Resources/AGENTS.md in the plugin folder, and its full text is at the end of this page. It tells an agent such as Claude Code, Codex or Cursor how to reach your open Unreal Editor, what to ask you, and every step of each build, with checks along the way. Everything it makes is assets and Blueprints in your own folder, with no C++.

Set it up

2 minutes, then the build
  1. 1

    Open your project

    Open the project in the Unreal Editor and leave it open. The agent works in the running editor.
  2. 2

    Hand your agent the guide

    Do one of these in your agent's chat:
    • Drag AGENTS.md from the plugin's Resources folder into it.
    • Paste this page's link: https://rigbak.com/docs/soulslike-combat/agents.
    • Type: "Read Resources/AGENTS.md in the SoulslikeEnemyCombat plugin folder before you start".
    A plugin installed from Fab sits in your engine folder, under Engine/Plugins/Marketplace/.
  3. 3

    Say yes to the one-time setup

    On first use the agent offers to point your project at the guide, so later sessions find it without being handed it. With your OK it adds one line naming the guide to CLAUDE.md and to AGENTS.md in your project folder: Claude Code reads the first, Codex and Cursor the second. For Claude Code it also copies the sec-agent skill into your project's .claude/skills folder, so typing /sec-agent starts a later session on the guide.
  4. 4

    Ask for an enemy

    Tell it what to build and where your assets are. It asks for anything it cannot work out from your asset names, builds step by step, and ends with a report of what it made and what it assumed.

How the agent reaches the editor

There are two ways in. The agent uses the Python bridge unless you ask for Epic's Unreal MCP, or your session already has it connected. Either way it asks your OK before it turns on a plugin or edits your project's config files, and tells you when the editor needs a restart.
RouteEngine versionsWhat it needs
Python bridge5.6, 5.7 and 5.8The engine's Python plugin and its remote execution, turned on in your project: two edits the agent makes, then one editor restart. The bridge itself, Resources/AgentBridge/run_in_editor.py, runs with the engine's own Python, so nothing gets installed.
Epic's Unreal MCP5.8The engine plugins ModelContextProtocol and AllToolsets turned on, their server starting with the editor, and an entry for it in your agent's MCP settings; then a restart of the editor and of the agent session. The plugin adds a toolset of its own, SECAgentTools, for the steps Epic's tools do not cover.
Other Unreal MCP servers are not supported, even when your agent has one connected.
On either route the plugin's agent tools make montages, add notify windows to them, make a locomotion Animation Blueprint for a skeleton that has none, and add blade sockets to a weapon mesh.

What it asks you

  1. The mesh: your Skeletal Mesh.
  2. The animations: a folder or a list. It sorts them by name into attacks, hit reactions, dodge, idle and locomotion, and asks when a name does not say which attack is light and which is heavy. It needs no death animation, since the enemy it builds goes limp when it dies; playing one instead is a Blueprint step on the enemy controller's On Death event, which it leaves to you.
  3. An Animation Blueprint for that skeleton with a DefaultSlot Slot node. With none, it makes one from the plugin's locomotion template.
  4. A weapon mesh, if the enemy holds one. An unarmed enemy hits with its hands.
  5. The folder and name: /Game/Enemies/<Name>/ unless you say otherwise.
It creates everything in that folder and never edits the plugin's own content: to change a shipped asset, it duplicates it into your folder first. Outside that folder it adds blade sockets to your weapon mesh and places the enemy in the level you have open.

What to ask it

AskWhat you get
"Build an enemy called DarkKnight from SKM_DarkKnight and the animations in /Game/DarkKnight/Anims."An enemy that notices you, keeps its distance, attacks by range, lands hits and flinches, placed in your level.
"Give it the sword SM_Sword."A weapon Blueprint with hit volumes along the blade, held by the enemy.
"Add a three-hit combo, and keep the heavy attack out of the opening move."More actions on its moveset, linked into a combo, with cooldowns.
"Make it parry my attacks some of the time."A parry reaction, and a warning on your attack montage for it to answer.
"Make a boss version that switches moves at half health."A boss with more health, a role of its own, a moveset whose second half starts at half health, and an arena that opens the fight.
"How do combat roles work?"An answer from these docs, with the page's link.
A few steps are left to you, and the agent names them in its report, such as the health bar a boss fight shows on screen.

What ships with the plugin

FileWhat it is
Resources/AGENTS.mdThe guide, in full below.
Resources/AgentBridge/run_in_editor.pyThe Python bridge: sends the agent's Python to the open editor and prints what the editor printed.
Resources/AgentSkills/sec-agent/SKILL.mdThe /sec-agent skill for Claude Code.

The rest of this page is the guide as the plugin ships it. An agent handed this page's link reads it from here.

Soulslike Enemy Combat: guide for AI agents

Soulslike Enemy Combat (SEC) is an Unreal Engine 5.6 to 5.8 plugin that gives a character souls-like enemy AI: it notices the player, keeps its distance, circles, picks attacks by range and timing, lands hits through swept weapon traces, and reacts to being hit. You build an enemy from data assets and a few Blueprints; no C++ is needed.
This file is written for an AI coding agent working in a customer's project while the Unreal Editor is open. Read all of it before you start. With the reference files in AgentGuide/ beside it, it is enough to build what it describes; do not search the plugin's source files.
Customer note: your agent may not find this file on its own. Fab installs the plugin under your engine's Engine/Plugins/Marketplace/ folder. Tell your agent "Read Resources/AGENTS.md in the SoulslikeEnemyCombat plugin folder before you start". The same text is at https://rigbak.com/docs/soulslike-combat/agents. On first use, the agent offers to point your project at this file, so later sessions find it on their own.

One-time setup in a project

The first time this guide is used in a project, offer the customer this setup, which lets later sessions find the guide without being handed it. Make it only with their OK. It does not hold up the build: if they have not answered, build first and offer it again in your final report. Skip it when the project's CLAUDE.md or AGENTS.md already names this guide.
  1. Find the plugin folder, the one holding SoulslikeEnemyCombat.uplugin: Plugins/SoulslikeEnemyCombat in the project folder, or, when Fab installed the plugin into the engine, a folder under Engine/Plugins/Marketplace/ in the engine folder ("The Python bridge" below, step 1, finds the engine folder).
  2. Point the project at this guide. Add this line to CLAUDE.md and to AGENTS.md in the project folder, making a file that is missing, with the plugin folder written out (relative to the project folder when the plugin is inside it): For Soulslike Enemy Combat, read <plugin folder>/Resources/AGENTS.md before building anything with it. Claude Code reads CLAUDE.md; Codex and Cursor read AGENTS.md.
  3. Claude Code only: the /sec-agent command. Copy the folder <plugin folder>/Resources/AgentSkills/sec-agent into the project's .claude/skills/ folder. Typing /sec-agent in a later session then starts it on this guide.

Connecting to the editor

Everything below runs in the customer's open Unreal Editor. There are two ways in. Use the Python bridge unless the customer asks for Epic's Unreal MCP or this session already has it connected. Other Unreal MCP servers are not supported, even when one is connected.

The Python bridge (UE 5.6, 5.7 and 5.8)

The plugin ships AgentBridge/run_in_editor.py in its Resources folder, beside this file. If this file reached you on its own, without the AgentBridge folder beside it, find the plugin folder ("One-time setup in a project", step 1) and use its Resources/AgentBridge. Run with the engine's own Python, it sends Python to the open editor and prints what the editor printed. It needs a shell and nothing installed.
  1. Read the project's .uproject. EngineAssociation is the engine version. For 5.6, the engine folder is the InstallLocation of the UE_5.6 entry in C:/ProgramData/Epic/UnrealEngineLauncher/LauncherInstalled.dat (JSON text despite its extension, so search it as text when a file reader refuses it), usually C:/Program Files/Epic Games/UE_5.6.
  2. Check the connection:
    "<engine folder>/Engine/Binaries/ThirdParty/Python3/Win64/python.exe" "<plugin>/Resources/AgentBridge/run_in_editor.py" --project "<path to the .uproject>" -c "import unreal; print(unreal.SystemLibrary.get_engine_version())"
    
    It prints the engine version when it is connected. In PowerShell, put & and a space before the quoted python.exe path.
  3. Exit code 2 means no editor answered. If the editor is closed, ask the customer to open the project. If it is open, the project needs a one-time setup. Ask the customer's OK, make these two edits, then ask them to restart the editor:
    • In the .uproject's Plugins list: { "Name": "PythonScriptPlugin", "Enabled": true }.
    • In Config/DefaultEngine.ini:
      [/Script/PythonScriptPlugin.PythonScriptPluginSettings]
      bRemoteExecution=True
Write each step's Python to a file and run it: ... run_in_editor.py --project "<path to the .uproject>" step.py. Exit code 1 means the Python raised; the traceback follows.
  • Names a run defines stay defined in the editor until it restarts. Run the setup block in "Driving the editor with Python" once per editor session, and again after a restart or a NameError.
  • The editor does nothing else while a run is going. Never wait or sleep inside the Python; start Play in one run and inspect it in the next.

Epic's Unreal MCP (UE 5.8)

Epic's Unreal MCP is two engine plugins, ModelContextProtocol and AllToolsets. Its tools are list_toolsets, describe_toolset and call_tool. To set it up, ask the customer's OK, make these edits, then ask them to restart the editor and this agent session:
  • In the .uproject's Plugins list: { "Name": "ModelContextProtocol", "Enabled": true } and { "Name": "AllToolsets", "Enabled": true }.
  • In Config/DefaultEditorPerProjectUserSettings.ini, so the server starts with the editor:
    [/Script/ModelContextProtocolEngine.ModelContextProtocolSettings]
    bAutoStartServer=True
  • The client entry, for Claude Code .mcp.json in the project root: { "mcpServers": { "unreal-mcp": { "type": "http", "url": "http://127.0.0.1:8000/mcp" } } }. Cursor reads the same shape from .cursor/mcp.json; Codex reads [mcp_servers.unreal-mcp] with url = "http://127.0.0.1:8000/mcp" from .codex/config.toml.
It runs no editor Python (its execute_tool_script only strings its own tools together in a sandbox), so use the tool table in "Driving the editor without Python". The plugin adds its own toolset, SECAgentTools (list_toolsets names it sec_agent_tools._toolset.SECAgentTools; pass that name as toolset_name), for the steps Epic's tools do not cover.

The plugin's agent tools

On both routes the plugin's agent tools make montages, notify windows and the locomotion Animation Blueprint, and add blade sockets to a weapon mesh. In Python they are the module sec_agent_tools, which ships with the plugin; on the Epic route they are the tools of the same names in SECAgentTools.

Before you start: ask the customer

Ask only what you cannot work out from asset names. State your assumptions in your final report so the customer can correct them.
An enemy they already have. A request to change one of the customer's enemies ("turn my knight into a boss") works on that enemy. Find it before building anything; build a new one only when there is none, and ask which one when there are several:
reg = unreal.AssetRegistryHelpers.get_asset_registry()
enemies = [str(a.package_name) for a in reg.get_assets_by_path("/Game", recursive=True)
           if (a.get_tag_value("NativeParentClass") or "").endswith(".EnemyCharacterBase'")]
print(enemies)
On Epic's MCP, list the customer's assets with AssetTools.find_assets and read each Blueprint's parent with BlueprintTools.get_parent. Then read what the enemy has, its AIConfig and that config's sets, and build on them.
  1. The mesh. The Skeletal Mesh asset path. Read its Skeleton from the mesh.
  2. The animations. A folder or a list. Sort them by name into attacks, hit reactions, dodge, idle and locomotion. Ask when names do not say which attack is light and which is heavy. A death animation is not needed: the duplicated enemy goes limp when it dies. Playing one instead is a Blueprint step on the enemy controller's On Death event; tell the customer.
  3. An Animation Blueprint for that skeleton, with a Slot node named DefaultSlot in its AnimGraph. If the customer has none, make one from the plugin's template ("No Animation Blueprint" below).
  4. A weapon mesh, if the enemy holds one. Unarmed enemies hit with their hands.
  5. The folder and name. Default: /Game/Enemies/<Name>/, with <Name> a short PascalCase name such as DarkKnight.

Rules

  • Create everything in the customer's folder. Do not edit or save anything under /SoulslikeEnemyCombat/; duplicate a plugin asset into the customer's folder when you need to change it.
  • Level steps work in the level open in the editor. If that level is the plugin's own (its path starts with /SoulslikeEnemyCombat/), ask the customer which of their levels to use, or make one in their folder.
  • Name assets <Prefix>_<Name>_<Purpose>: BP_DarkKnight, AM_DarkKnight_Attack01, ActionSet_DarkKnight, ReactionSet_DarkKnight, EnemyConfig_DarkKnight, Awareness_DarkKnight, Damage_DarkKnight_Light, BP_DarkKnight_Sword.
  • Save each asset as soon as you finish it.
  • Check each step as described before you move on. A wrong value fails silently in several places; the checks catch it.
  • Use the exact property names in this file. set_editor_property accepts the C++ name (SocketIDs, bUseAsWeapon) as well as the Python name.
  • In Python, a C++ type drops its U, A, F or E prefix: UEnemyAIConfig is unreal.EnemyAIConfig, FActionSpec is unreal.ActionSpec. Enum values are upper snake case: ESECTraceShape::CapsuleTwoPoint is unreal.SECTraceShape.CAPSULE_TWO_POINT. The engine's animation functions are unreal.AnimationLibrary.
  • An ability that SEC runs must reach End Ability on every path: completion, cancel and interrupt. The shipped GA_SEC_MontageAbility does; check any custom ability for it.
  • Give unreal.new_object the asset that will hold the object as its outer, never a class default object (unreal.get_default_object(...), defaults(...)), not even to look at a class's properties: a later object of that class with the same name takes it as its template, and the asset holding that object fails to save.

Driving the editor with Python

Run this once at the start of each Python session. Every later snippet uses it.
import unreal
import sec_agent_tools as sec
EAL = unreal.EditorAssetLibrary
ANIM = unreal.AnimationLibrary
TOOLS = unreal.AssetToolsHelpers.get_asset_tools()
 
def load(path):
    obj = EAL.load_asset(path)
    if obj is None:
        raise RuntimeError(f"No asset at {path}")
    return obj
 
def bp_class(path):
    return EAL.load_blueprint_class(path)
 
def new_data_asset(cls, folder, name):
    factory = unreal.DataAssetFactory()
    factory.set_editor_property("DataAssetClass", cls)
    return TOOLS.create_asset(name, folder, cls, factory)
 
def duplicate(source_path, folder, name):
    return EAL.duplicate_asset(source_path, f"{folder}/{name}")
 
def defaults(bp):
    return unreal.get_default_object(bp.generated_class())
 
def bp_component(bp, name_start):
    sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)
    lib = unreal.SubobjectDataBlueprintFunctionLibrary
    for handle in sds.k2_gather_subobject_data_for_blueprint(bp):
        obj = lib.get_associated_object(lib.get_data(handle))
        if obj and obj.get_name().startswith(name_start):
            return obj
    return None
 
def tag(name):
    t = unreal.GameplayTag()
    t.import_text(name)
    return t
 
def tags(*names):
    return unreal.GameplayTagLibrary.make_gameplay_tag_container_from_array([tag(n) for n in names])
 
def length(anim):
    return ANIM.get_sequence_length(anim)
 
def save(*assets):
    for a in assets:
        if not EAL.save_loaded_asset(a, False):
            raise RuntimeError(f"{a.get_path_name()} did not save; log_warnings('EditorErrors') says why")
 
def log_warnings(prefix="LogSEC"):
    name = unreal.Paths.get_base_filename(unreal.Paths.get_project_file_path())
    path = unreal.Paths.project_log_dir() + name + ".log"
    with open(path, encoding="utf-8", errors="ignore") as f:
        return [l.strip() for l in f if prefix in l and ("Warning" in l or "Error" in l)]
The agent tools you call through sec:
CallDoes
sec.create_montage_from_animation(sequence, folder, name)Makes a montage on the DefaultSlot slot, turns the sequence's root motion on, saves both.
sec.add_notify_state_to_montage(montage, notify_class, start_seconds, duration_seconds)Adds a notify window on a Combat track and returns it, so you can set its properties. Save the montage afterwards.
sec.create_locomotion_anim_blueprint(skeleton, folder, name, idle, walk, run)Makes an Animation Blueprint for a skeleton that has none, and saves it.
sec.add_blade_sockets(static_mesh)Adds the TraceStart and TraceEnd sockets a blade trace sweeps between, saves the mesh, and says where they sit.
sec.set_montage_sections(montage, sections), sec.add_gameplay_tag(tag, comment), sec.validate_asset(asset)Montage sections, a new gameplay tag, an asset's own checks as text: AgentGuide/Python.md.
Each call raises with a sentence saying what is wrong when it cannot do its job; read it and fix that before going on. log_warnings() is how you read SEC's setup warnings after a Play session. Each warning names the asset and the fix.

Driving the editor without Python (Epic Unreal MCP, UE 5.8)

Epic's MCP lists three tools: list_toolsets, describe_toolset and call_tool. Use describe_toolset to read the exact argument names. In call_tool, toolset_name is the name list_toolsets prints, such as editor_toolset.toolsets.asset.AssetTools, and tool_name is the tool's own name, such as find_assets. Map each Python step to these tools:
Python stepEpic Unreal MCP tool
load, find assetsAssetTools.find_assets, AssetTools.load_asset
duplicateAssetTools.duplicate
new_data_assetDataAssetTools.create
Blueprint with a parent classBlueprintTools.create
defaults(bp) and setting class defaultsBlueprintTools.get_default_object, then ObjectTools.set_properties
Instanced objects (selectors, execution methods, scorers)ObjectTools.set_properties with the class path, as below
Add a component to a BlueprintActorTools.add_component
saveAssetTools.save_assets
Play and lookEditorAppToolset.StartPIE, StopPIE, CaptureViewport
log_warningsLogsToolset.GetLogEntries, as below
sec.*SECAgentTools tools of the same names. add_notify_state_to_montage takes the class path (for example /Script/SECCore.SECMeleeTraceNotifyState) and a track name (Combat).
The property names and values in every step are the same on both routes. How Epic's tools take values:
  • An asset is its object path: the package path, a dot, and the asset name again, as in /Game/Enemies/DarkKnight/AM_DarkKnight_Light.AM_DarkKnight_Light. The package path alone is refused.
  • A class is /Script/<Module>.<Name>, the C++ name without its first letter. SEC's classes are in three modules. SECMotion holds the approach window: /Script/SECMotion.SECApproachWindowNotifyState. SECCore holds damage configs, the melee trace, telegraph and state tag windows, telegraph shapes, the gameplay event and anim effect notifies, vitals and teams: /Script/SECCore.SECMeleeTraceNotifyState. SoulslikeEnemyCombat holds the rest (action and reaction sets, the AI config and the awareness config, movement profiles, execution methods, payloads, scorers, gates, selectors, weapons, the encounter trigger): /Script/SoulslikeEnemyCombat.ActionSet, /Script/SoulslikeEnemyCombat.SECAwarenessConfig.
  • An instanced object (an execution method, a payload, a scorer, a selector) is made by giving its class path as a plain string: {"TargetSelector": "/Script/SoulslikeEnemyCombat.AwarenessFilteredTargetSelector"}, or in an array, {"Scorers": ["/Script/SoulslikeEnemyCombat.SECDistanceScorer"]}. Properties written beside the class path are dropped. Read the owner back with ObjectTools.get_properties, which gives the new object's path ( .../ActionSet_DarkKnight.ActionSet_DarkKnight:SECAbilityExecutionMethod_0), and set its own properties on that path.
  • An array changes one way per ObjectTools.set_properties call: elements added or removed while every other element stays exactly as get_properties returned it, or elements changed at the same length. Keep an existing instanced element as {"refPath": "<its path>"}. A call refused as ambiguous does both at once; split it in two.
  • Log lines come from LogsToolset.GetLogEntries, which requires pattern, a regular expression, and reads only its own category unless category is empty: {"category": "", "pattern": "LogSEC[A-Za-z]*: (Warning|Error)"}.

Recipe 1: an enemy from the customer's mesh and animations

Set these first:
F = "/Game/Enemies/DarkKnight"      # the customer's folder
N = "DarkKnight"                     # the enemy's name
mesh = load("/Game/.../SKM_DarkKnight")
seq_light = load("/Game/.../Attack_01")
seq_heavy = load("/Game/.../Attack_03")
seq_hit = load("/Game/.../Hit_Front")
abp = load("/Game/.../ABP_DarkKnight")   # the customer's Animation Blueprint, or the one "No Animation Blueprint" makes

Step 1. Check the skeleton

The weapon attaches to a bone or socket named hand_r unless you say otherwise.
print(ANIM.does_bone_name_exist(seq_light, "hand_r"))
Check: prints True. If not, find the right hand bone's name with the customer and use it as the weapon's AttachSocket in Recipe 2.

Step 2. Make a montage per animation

SEC plays montages, not bare sequences. Make one for each attack, each hit reaction and the dodge, if there is one.
am_light = sec.create_montage_from_animation(seq_light, F, f"AM_{N}_Light")
am_heavy = sec.create_montage_from_animation(seq_heavy, F, f"AM_{N}_Heavy")
am_hit = sec.create_montage_from_animation(seq_hit, F, f"AM_{N}_HitFront")
The tool turns root motion on for the sequence, so an attack that steps forward moves the enemy, and saves the montage. It plays on the DefaultSlot slot.
Check: length(am_light) is above 0.

Step 3. Make the damage config

A Damage Config (USECDamageConfig) says what one hit is worth. Six ship under /SoulslikeEnemyCombat/Core/Damage/ (Damage_SEC_Slashing, _Piercing, _Bludgeoning, _Fire, _Ice, _Lightning). Make your own to set the amount:
dmg_light = new_data_asset(unreal.SECDamageConfig, F, f"Damage_{N}_Light")
dmg_light.set_editor_property("Damage", 15.0)
dmg_light.set_editor_property("DamageTags", tags("SEC.Damage.Physical.Slashing"))
dmg_heavy = new_data_asset(unreal.SECDamageConfig, F, f"Damage_{N}_Heavy")
dmg_heavy.set_editor_property("Damage", 30.0)
dmg_heavy.set_editor_property("DamageTags", tags("SEC.Damage.Physical.Slashing"))
save(dmg_light, dmg_heavy)

Step 4. Mark the strike frames on each attack montage

Two windows on each attack:
  • SEC Approach Window (USECApproachWindowNotifyState) over the wind-up. It turns the enemy to its target and glides it into reach, so an attack chosen from a few metres away still lands. Its defaults (Move And Face, toward Target) are right. Set its StopDistanceOverride: the gap in cm it leaves between the two bodies, which has to be shorter than the weapon reaches past the enemy's own body. About 40 suits a sword, one-handed or two-handed, and 20 fists; only a spear or a polearm takes more. Left at -1 it uses the approach component's 150, and a sword swings short of the target. Without the window the enemy swings from where it stands and misses.
  • SEC Melee Trace Window (USECMeleeTraceNotifyState) over the frames where the weapon connects. Nothing takes damage without it.
Without frame data from the customer, put the approach window from 10% to 30% of the montage and the trace window from 30% for 25% of its length; tell the customer the times you picked.
for am, dmg in ((am_light, dmg_light), (am_heavy, dmg_heavy)):
    L = length(am)
    a = sec.add_notify_state_to_montage(am, unreal.SECApproachWindowNotifyState, L * 0.10, L * 0.20)
    a.set_editor_property("StopDistanceOverride", 40.0)   # cm between the two bodies; fit it to the weapon
    w = sec.add_notify_state_to_montage(am, unreal.SECMeleeTraceNotifyState, L * 0.30, L * 0.25)
    w.set_editor_property("SocketIDs", ["Blade"])   # the weapon's trace socket ID; [] sweeps every registered socket
    w.set_editor_property("DamageConfig", dmg)
    save(am)
For an unarmed enemy, use the socket IDs from "Unarmed enemies" in Recipe 2.
Check: ANIM.get_animation_notify_events(am_light) lists two notify states, SECApproachWindowNotifyState and SECMeleeTraceNotifyState.

Step 5. Make the enemy Blueprint

Duplicate the demo enemy. It already carries what an enemy needs beyond the base class: a Motion Warping component, the damage handler that tells the enemy it was hit (which drives flinch reactions), its controller and a trace team filter.
bp = duplicate("/SoulslikeEnemyCombat/Demo/Characters/Enemy/BP_SEC_EnemyCharacter", F, f"BP_{N}")
d = defaults(bp)
body = d.get_editor_property("Mesh")
body.set_editor_property("SkeletalMeshAsset", mesh)
body.set_editor_property("AnimClass", abp.generated_class())
d.set_editor_property("StartingAbilities", [])
d.get_editor_property("MeleeTraceComponent").set_editor_property("DefaultDamageConfig", dmg_light)
d.get_editor_property("CharacterMovement").set_editor_property("MaxWalkSpeed", 400.0)
save(bp)
  • StartingAbilities is emptied because the demo's stagger and stun abilities (GA_SEC_Stagger, GA_SEC_Stunned) play the demo mannequin's montages, which would make the customer's character perform the demo's stagger and stun. Without them the enemy is never stunned; a flinch on the customer's own animation comes from the Reaction Set in Step 7.
  • The duplicate keeps the demo's AI controller, BP_SEC_EnemyController, and its starting weapon. Recipe 2 replaces the weapon.
  • The duplicate keeps the demo mannequin's mesh placement, which fits a character built like the UE5 mannequin (origin at the feet, facing +Y). If the customer's character floats, sinks or faces sideways, adjust the Mesh component's RelativeLocation and RelativeRotation, and CapsuleHalfHeight and CapsuleRadius on CapsuleComponent.
Check: open the Blueprint's viewport, or capture it, and confirm the mesh stands inside the capsule facing along the blue arrow.

Step 6. Make the Action Set (the moveset)

An Action Set (UActionSet) holds the enemy's attacks in Actions, an array of FActionSpec. For each action set:
  • ActionId: a short unique name. An empty ID is dropped silently.
  • ExecutionMethod: a Gameplay Ability method (USECAbilityExecutionMethod) whose AbilityClass is the shipped GA_SEC_MontageAbility, with a Montage Payload (USECMontagePayload) whose Montage is the attack montage.
  • A Distance Scorer (USECDistanceScorer) in CustomScoring.Scorers. A new action has no scorers and fires at any distance, so every melee attack needs one.
montage_ability = bp_class("/SoulslikeEnemyCombat/Core/Abilities/GA_SEC_MontageAbility")
 
def montage_method(owner, montage, play_rate=1.0):
    method = unreal.new_object(unreal.SECAbilityExecutionMethod, outer=owner)
    method.set_editor_property("AbilityClass", montage_ability)
    payload = method.get_editor_property("Payload")
    if payload is None:
        payload = unreal.new_object(unreal.SECMontagePayload, outer=method)
        method.set_editor_property("Payload", payload)
    payload.set_editor_property("Montage", montage)
    payload.set_editor_property("PlayRate", play_rate)
    return method
 
def range_scorer(owner, cls, min_v, opt_min, opt_max, max_v):
    scorer = unreal.new_object(cls, outer=owner)
    r = scorer.get_editor_property("Range")
    for key, v in (("MinValue", min_v), ("OptimalMin", opt_min), ("OptimalMax", opt_max), ("MaxValue", max_v)):
        r.set_editor_property(key, v)
    scorer.set_editor_property("Range", r)
    return scorer
 
def action(owner, action_id, montage, scorers, cooldown=0.0, weight=1.0, risk=1.0, play_rate=1.0, chain_to=None):
    spec = unreal.ActionSpec()
    spec.set_editor_property("ActionId", action_id)
    spec.set_editor_property("ExecutionMethod", montage_method(owner, montage, play_rate))
    scoring = spec.get_editor_property("CustomScoring")
    scoring.set_editor_property("Scorers", scorers)
    spec.set_editor_property("CustomScoring", scoring)
    spec.set_editor_property("SelectionWeight", weight)
    spec.set_editor_property("RiskPenalty", risk)
    cd = spec.get_editor_property("Cooldown")
    cd.set_editor_property("Duration", cooldown)
    spec.set_editor_property("Cooldown", cd)
    if chain_to:
        link = unreal.ActionChainLink()
        link.set_editor_property("TargetActionId", chain_to)
        link.set_editor_property("BonusMultiplier", 1.5)
        spec.set_editor_property("ChainLinks", [link])
    return spec
 
aset = new_data_asset(unreal.ActionSet, F, f"ActionSet_{N}")
aset.set_editor_property("Actions", [
    action(aset, "Light", am_light, [range_scorer(aset, unreal.SECDistanceScorer, 0, 100, 250, 450)],
           cooldown=1.5, chain_to="Heavy"),
    action(aset, "Heavy", am_heavy, [range_scorer(aset, unreal.SECDistanceScorer, 0, 150, 300, 500)],
           cooldown=5.0, weight=0.6, risk=1.5),
])
save(aset)
Setting AbilityClass on the method makes its Montage Payload by itself; montage_method only fills it.
What the numbers mean:
  • A Range scores 1 between OptimalMin and OptimalMax and falls to 0 at MinValue and MaxValue. Distances are in centimetres.
  • SelectionWeight under 1 and RiskPenalty over 1 make an action come up less often on its own.
  • The chain link makes Heavy score 50% higher for a short window after Light completes, so the enemy combos.
  • Each new action already holds SEC.State.IsMovementBlocked in AddTags, which stops the enemy walking while it swings. Leave it.
Check: reload with load(...) and print len(aset.get_editor_property("Actions")), each action's ActionId, and that its ExecutionMethod and payload Montage are set.

Step 7. Make the Reaction Set (the flinch)

A Reaction Set (UReactionSet) holds Reactions (FReactionSpec) and the TriggerRules (FSECReactionTriggerRule) that pick one. A set with no trigger rules reacts to nothing.
rset = new_data_asset(unreal.ReactionSet, F, f"ReactionSet_{N}")
flinch = unreal.ReactionSpec()
flinch.set_editor_property("ReactionId", "Flinch")
flinch.set_editor_property("ExecutionMethod", montage_method(rset, am_hit))
cd = flinch.get_editor_property("Cooldown")
cd.set_editor_property("Duration", 1.5)
flinch.set_editor_property("Cooldown", cd)
 
rule = unreal.SECReactionTriggerRule()
rule.set_editor_property("RuleId", "FlinchOnHit")
rule.set_editor_property("OnStimulus", tag("SEC.Stimulus.DamageTaken"))
link = unreal.SECReactionTriggerLink()
link.set_editor_property("ReactionId", "Flinch")
rule.set_editor_property("ReactionLinks", [link])
rule.set_editor_property("Chance", 0.6)
 
rset.set_editor_property("Reactions", [flinch])
rset.set_editor_property("TriggerRules", [rule])
save(rset)
SEC.Stimulus.DamageTaken reaches the enemy because the duplicated demo Blueprint reports each hit it takes. A Blueprint made from Enemy Character Base directly does not; its Handle Incoming Damage must call Resolve Enemy Damage, a Blueprint edit the customer makes by hand.
Check: print the reaction's ReactionId and the rule's OnStimulus.

Step 8. Make the AI Config

An Enemy AI Config (UEnemyAIConfig) ties the sets together. Two fields ship empty and matter: TargetSelector and AwarenessConfig.
awareness = new_data_asset(unreal.SECAwarenessConfig, F, f"Awareness_{N}")   # defaults: 1500 cm sight, 180 degree cone, 1200 cm hearing
movement = new_data_asset(unreal.MovementBehaviorProfile, F, f"Movement_{N}")
movement.set_editor_property("DesiredDistance", 300.0)
 
config = new_data_asset(unreal.EnemyAIConfig, F, f"EnemyConfig_{N}")
config.set_editor_property("TargetSelector", unreal.new_object(unreal.AwarenessFilteredTargetSelector, outer=config))
config.set_editor_property("AwarenessConfig", awareness)
config.set_editor_property("DefaultActionSet", aset)
config.set_editor_property("DefaultReactionSet", rset)
config.set_editor_property("DefaultMovementProfile", movement)
save(awareness, movement, config)
 
d = defaults(bp)
d.set_editor_property("AIConfig", config)
save(bp)
  • StateTree (shown as Default State Tree) stays empty, which runs SEC's built-in brain: notice, investigate, close in, circle, attack, react, plus patrol and the walk home once the placed enemy has territory posts (AgentGuide/AwarenessTerritory.md). The demo's /SoulslikeEnemyCombat/Demo/AI/StateTrees/ST_SEC_Core runs patrol, investigation, the fight and the walk home as a State Tree; set it only when the customer wants to edit the brain in the State Tree editor.
  • The awareness config does the sensing, and the filtered selector turns what the enemy sensed into its target. A player the enemy detects joins the target pool on its own, so the player Blueprint needs no change.
  • Sight skips actors on the enemy's own team. Enemies default to Team Id 1; a player with no SEC Team component counts as neutral and is seen.
Check: print config.get_editor_property("TargetSelector"); it is not None.

Step 9. Put it in a level

world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
has_nav = any(isinstance(a, unreal.NavMeshBoundsVolume) for a in actors.get_all_level_actors())
enemy = actors.spawn_actor_from_class(bp.generated_class(), unreal.Vector(800, 0, 100))
unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
The enemy walks on a navmesh. If has_nav is False, spawn a NavMeshBoundsVolume over the floor and scale it to cover the play area, or ask the customer to add one. Navigation builds on its own in the editor.

Step 10. Play and check

les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
les.editor_request_begin_play()
Play starts once this run returns. Check it in the next run, a few seconds later:
game = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_game_world()
unreal.SystemLibrary.execute_console_command(game, "SEC.Debug.All 1")
unreal.AutomationLibrary.take_high_res_screenshot(1280, 720, f"{N}_check.png")
Then end Play and read the warnings:
les.editor_request_end_play()
print(log_warnings())
Success looks like: walking into the enemy's view turns its awareness line to Detected; it closes to about the movement profile's distance, circles, swings its light attack within range, sometimes follows with the heavy, flinches some of the time when hit, and a hit on the player lands. log_warnings() returns nothing about this enemy.
Ask the customer to play it too: only a person can judge whether the strike windows sit on the right frames. SEC.Debug.Melee.DrawTracing 1 draws each sweep.

Recipe 2: a weapon

Step 1. The weapon Blueprint

Duplicate the demo weapon base. It carries a Static Mesh component and a trail effect.
sword_mesh = load("/Game/.../SM_Sword")
wbp = duplicate("/SoulslikeEnemyCombat/Demo/Weapons/BP_SEC_WeaponBase", F, f"BP_{N}_Sword")
bp_component(wbp, "StaticMesh").set_editor_property("StaticMesh", sword_mesh)
save(wbp)

Step 2. Sockets on the weapon mesh

A blade is traced by a two-point capsule between two sockets on its mesh, by default named TraceStart (above the guard) and TraceEnd (the tip). Check, and add them when missing:
for name in ("TraceStart", "TraceEnd"):
    print(name, sword_mesh.find_socket(name) is not None)
 
print(sec.add_blade_sockets(sword_mesh))
This places TraceStart near the guard and TraceEnd near the tip, and saves the mesh. It changes the customer's mesh: tell them, and ask them to check the sockets sit on the blade when the grip is not at the mesh's origin.

Step 3. Trace sockets and attachment

w = defaults(wbp)
s = unreal.SECTraceSocket()
s.set_editor_property("ID", "Blade")
s.set_editor_property("Shape", unreal.SECTraceShape.CAPSULE_TWO_POINT)
s.set_editor_property("SocketOrBoneName", "TraceStart")
s.set_editor_property("EndSocketOrBoneName", "TraceEnd")
s.set_editor_property("Radius", 8.0)
w.set_editor_property("TraceSockets", [s])
w.set_editor_property("AttachSocket", "hand_r")
save(wbp)
  • ID is what each melee trace window lists in SocketIDs ("Blade" in Recipe 1). The names must match exactly.
  • AttachOffset (a transform) fixes a weapon held at the wrong angle. Author it only after looking at a capture.

Step 4. Give it to the enemy

entry = unreal.SECEquipLoadoutEntry()
entry.set_editor_property("EquippableClass", wbp.generated_class())
entry.set_editor_property("bUseAsWeapon", True)
equip = defaults(bp).get_editor_property("EquipmentComponent")
equip.set_editor_property("StartingLoadout", [entry])
save(bp)
bUseAsWeapon off spawns and attaches the weapon but registers none of its trace sockets, so swings pass through.
Check: in Play, the sword sits in the right hand, and SEC.Debug.Melee.DrawTracing 1 draws a capsule along the blade during each strike window.

Unarmed enemies

The pawn's melee trace has no data field for its own sockets; a Blueprint registers them at Begin Play with Register Sphere Socket. To stay data-only, give the enemy a "fists" weapon with no mesh whose trace sockets sit on the body's hand bones:
  1. Add the tag Body to the pawn Mesh's ComponentTags.
  2. Duplicate BP_SEC_WeaponBase as BP_<Name>_Fists, clear its Static Mesh component's StaticMesh, and give it two TraceSockets rows: ID HandR and HandL, Shape Sphere, SocketOrBoneName hand_r and hand_l, SourceComponentTag Body, Radius 12.
  3. Equip it as in Step 4, and list HandR (or both) in each window's SocketIDs.
A socket's SourceComponentTag is looked up on the enemy and on everything attached to it, which is how the fists' sockets find the body mesh.

A two-handed weapon enemy

Same steps, with these values:
  • AttachSocket stays hand_r; set AttachOffset so the off hand meets the grip. The animations decide the pose.
  • Slower, heavier attacks: Damage 30 to 45, distance ranges 150 to 350, Cooldown.Duration 3 to 6, PlayRate 0.9 on the payloads.
  • StopDistanceOverride stays about 40 on each approach window. A heavier weapon does not stand farther off: at a 60 cm gap a two-handed swing of the same length falls short and never lands.
  • DesiredDistance 350 on the movement profile.
  • A weapon can carry its own moveset: set WeaponActionSet on the weapon Blueprint's defaults and it wins over the AI Config while equipped. WeaponReactionSet does the same for reactions.

Recipe 3: more attacks, combos and pacing

Add actions to the same Actions array. Useful settings:
WantSet
An attack for a rangeA Distance Scorer with its own band. Give each attack a different band.
Only when facing the targetAdd an Angle Scorer (USECAngleScorer); its default Range peaks at 0 to 30 degrees.
A three-hit comboChainLinks from A to B and B to C. For a link that must follow at once, set the link's Pacing to unreal.SECChainPacing.IMMEDIATE.
A move that only follows anotherSelection = unreal.SECActionSelection.CHAIN_ONLY
Fewer repeatsCooldown.Duration, and Cooldown.MaxConsecutiveUses (-1 is unlimited)
Not as the opening moveCooldown.InitialCooldown
A gap between any two attacksActionRecoveryTime on the AI Config (0 by default)
Several enemies take turnsAdd a Combat Token Gate (USECTokenGate, TokenTag = SEC.Token.Attack) to CustomScoring.Gates
Back off when crowdedAn action whose execution method runs a behavior tree with the Make Distance task, Distance Scorer 0 to 100 cm. See the Action System page.
An attack that turns and closes the gapA SEC Approach Window (USECApproachWindowNotifyState) over the wind-up of the montage

Recipe 4: reactions

Flinch on hit

Recipe 1, Step 7. For hits from the front and from behind, make one reaction each and gate them with an Incoming Angle Gate (USECGate_IncomingAngle) in the reaction's CustomScoring.Gates: HalfAngleDegrees 90 on the front reaction, and the same with bInvert on for the back one. The gate measures an arc to either side of straight ahead, so it cannot tell left from right; a side hit plays the front or back reaction.

Parry or dodge an incoming swing

An enemy can only answer a swing that announces itself. The attacker's montage needs a SEC Telegraph Window (USECTelegraphNotifyState) over its wind-up, with Broadcast.Shape set to a Sphere (USECTelegraphShape_Sphere, Radius 500 by default). A telegraph with no shape reaches nobody.
t = sec.add_notify_state_to_montage(player_attack_montage, unreal.SECTelegraphNotifyState, 0.0, strike_start)
b = t.get_editor_property("Broadcast")
b.set_editor_property("Shape", unreal.new_object(unreal.SECTelegraphShape_Sphere, outer=t))
t.set_editor_property("Broadcast", b)
t.set_editor_property("bFindAttackAutomatically", True)
save(player_attack_montage)
Then add a Parry (or Dodge) reaction playing its montage, and a trigger rule with OnStimulus = SEC.Stimulus.AttackTelegraphed linking to it, Chance 0.3 to 0.5 and MinInterval 2. For i-frames on a dodge, put a SEC State Tag Window (USECStateTagNotifyState) holding SEC.State.Intangible over the dodge's evasive frames.

Recipe 5: a boss

All data, no Blueprint graph edits.
  1. Pawn. Duplicate BP_<Name> as BP_<Name>_Boss. Raise its health on the Vitals component:
    vit = defaults(boss_bp).get_editor_property("Vitals")
    rows = vit.get_editor_property("Vitals")
    for r in rows:
        if str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("VitalTag"))) == "SEC.Vital.Health":
            r.set_editor_property("MaxValue", 600.0)
    vit.set_editor_property("Vitals", rows)
    save(boss_bp)
    For a bigger boss, scale the Mesh and the capsule together.
  2. Role. Duplicate the AI Config as EnemyConfig_<Name>_Boss and point the boss pawn's AIConfig at it. In its RoleRegistrationParams set AllowedRoles = tags("SEC.Role.Elite"), PreferredRole = tag("SEC.Role.Elite"), Priority = 200. Set bIgnoreTargetRedistribution = True. Elite slots are unlimited by default, so the boss does not wait for a turn.
  3. Phases by health. Make ActionSet_<Name>_Boss. Give phase one actions a Health Scorer (USECHealthScorer) with Range 0.49, 0.5, 1.0, 1.0, and phase two actions one with Range 0.0, 0.0, 0.5, 0.51. Health is a 0 to 1 fraction, so phase one stops and phase two starts at half health. Phase two can reuse the same montages with PlayRate 1.25 and shorter cooldowns, plus a new finisher. Set it as the boss config's DefaultActionSet.
  4. Arena. Spawn the demo's boss arena trigger, /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger (on Epic's MCP its class is /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger.BP_SEC_BossArenaTrigger_C), covering the arena. It is an SEC Encounter Trigger that ends the fight as Defeated once every enemy it lists is dead or gone. Set EncounterName (text), bTriggerOnce = True, bEndWhenPlayerLeaves = False, and Enemies = [placed boss actor]. Enemies can only be set on the placed actor, not on a Blueprint. Size its box with trigger.get_editor_property("TriggerBox").set_box_extent(unreal.Vector(x, y, z)).
  5. Fight from the start. For an arena where the boss knows about the player at once, set bAlwaysAware = True on a dedicated awareness config for the boss.
What needs a person: a health bar on screen needs a widget bound to the SEC Encounter Subsystem's On Encounter Started (the demo's WBP_SEC_HUD_Encounter shows how). Tell the customer it is a Blueprint step.

No Animation Blueprint

An enemy needs an Animation Blueprint on its skeleton that blends idle, walk and run by speed and has a DefaultSlot Slot node, which is where every SEC montage plays. If the customer has none, ask whether one came with the animation pack under another name. Otherwise make one from the plugin's locomotion template:
anims = "/Game/.../"
abp = sec.create_locomotion_anim_blueprint(mesh.get_editor_property("Skeleton"), F, f"ABP_{N}",
                                           load(anims + "Idle"), load(anims + "Walk_Fwd"), load(anims + "Run_Fwd"))
The new Animation Blueprint blends idle into walk by 200 cm/s and walk into run by 500 cm/s. To match the enemy's own speeds (MaxWalkSpeed on its CharacterMovement), set WalkSpeed and RunSpeed on its class defaults (defaults(abp)) and save it.

More tasks: the reference files

The recipes above build the common cases. For anything else, open the one file below that covers the task. Each lists its tasks first, then recipes with a check, the exact class and property names, and the common mistakes. They sit in AgentGuide/ beside this file, in the plugin's Resources folder; when this file reached you on its own, open them there.
TaskFile
A property refuses a value, rows in arrays, components on a Blueprint, montage sections, new gameplay tags, checking an asset, project settingsAgentGuide/Python.md
A moveset from several animations, ranges, combos and pacing, gates, cooldowns, one clip with several attacks, movesets per weapon or roleAgentGuide/Actions.md
Flinches by direction, parry and dodge rules, what an enemy can noticeAgentGuide/Reactions.md
Keeping distance, circling, backing off when stared at, flanking anglesAgentGuide/Movement.md
Target selection, combat roles and limits, taking turns, teams, the brain, repeatable fightsAgentGuide/BrainRolesTargets.md
Sight and hearing, patrols, guard posts, leashesAgentGuide/AwarenessTerritory.md
An attack run as a behavior tree, the State Tree tasksAgentGuide/BehaviorTrees.md
Montage windows, root-motion lunges, step-backs, tag-driven animation, walking slower on a tagAgentGuide/Animation.md
Weapons and loadouts, damage and hit effects, enemies that block and parryAgentGuide/WeaponsDamageDefense.md
Health, stamina and stance, death, encounters and boss bars, settings, debuggingAgentGuide/VitalsEncountersDebug.md

Reference

Assets you create

AssetClassMade withHolds
Enemy pawnBlueprint of AEnemyCharacterBase (duplicate of BP_SEC_EnemyCharacter)duplicateMesh, AIConfig, StartingAbilities, components below
AI controllerAEnemyControllerBase; the demo BP_SEC_EnemyController is finealready set on the duplicateMovement, scoring, reactions, awareness, the brain
Enemy AI ConfigUEnemyAIConfignew_data_assetTargetSelector, AwarenessConfig, DefaultActionSet, DefaultReactionSet, DefaultMovementProfile, StateTree, RoleRegistrationParams, ActionRecoveryTime
Action SetUActionSetnew_data_assetActions (FActionSpec)
Reaction SetUReactionSetnew_data_assetReactions (FReactionSpec), TriggerRules (FSECReactionTriggerRule)
Awareness ConfigUSECAwarenessConfignew_data_assetSight, hearing, bAlwaysAware
Movement Behavior ProfileUMovementBehaviorProfilenew_data_assetDesiredDistance (cm)
Damage ConfigUSECDamageConfignew_data_assetDamage, DamageTags, HitEffects
WeaponBlueprint of ASECWeaponBase (duplicate of BP_SEC_WeaponBase)duplicateTraceSockets, AttachSocket, AttachOffset, WeaponActionSet, WeaponReactionSet
Animation BlueprintChild of ABP_SEC_LocomotionTemplatesec.create_locomotion_anim_blueprintIdleAnimation, WalkAnimation, RunAnimation, WalkSpeed, RunSpeed
MontageUAnimMontagesec.create_montage_from_animationNotify windows below
Pawn components you touch (all on the duplicated pawn): Mesh, CharacterMovement, MeleeTraceComponent (DefaultDamageConfig), EquipmentComponent (StartingLoadout), Vitals (Vitals rows: VitalTag, MaxValue, RegenRatePerSecond, RegenDelaySeconds; one SEC.Vital.Health row at 100 by default).

Shipped assets you reuse

AssetPath
Locomotion template/SoulslikeEnemyCombat/Core/Animation/ABP_SEC_LocomotionTemplate
Montage-playing ability/SoulslikeEnemyCombat/Core/Abilities/GA_SEC_MontageAbility
Damage configs/SoulslikeEnemyCombat/Core/Damage/Damage_SEC_Slashing and the five others
Demo enemy and controller/SoulslikeEnemyCombat/Demo/Characters/Enemy/BP_SEC_EnemyCharacter, BP_SEC_EnemyController
Demo weapon base/SoulslikeEnemyCombat/Demo/Weapons/BP_SEC_WeaponBase
Demo brain/SoulslikeEnemyCombat/Demo/AI/StateTrees/ST_SEC_Core
Boss arena trigger/SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger
Demo awareness/SoulslikeEnemyCombat/Demo/AI/Awareness_SEC_Default
Demo movement profiles/SoulslikeEnemyCombat/Demo/AI/MovementProfiles/MovementProfile_SEC_Attacker (and _Flanker, _Waiter, _Supporter, _Elite)
Demo level/SoulslikeEnemyCombat/Demo/Maps/LVL_SEC_Demo
The demo content must stay in the project while anything references it. Duplicate a demo asset into the customer's folder when the customer plans to remove the demo.

Montage notifies

Notify (editor name)ClassNeeded forKey properties
SEC Melee Trace WindowUSECMeleeTraceNotifyStateAny hit to deal damageSocketIDs, DamageConfig, ExtraDamageTags
SEC Telegraph WindowUSECTelegraphNotifyStateEnemies parrying or dodging this attackBroadcast.Shape, bFindAttackAutomatically, Tags
SEC State Tag WindowUSECStateTagNotifyStateTags for a stretch: SEC.State.Intangible for i-framesTags
SEC Approach WindowUSECApproachWindowNotifyStateTurning and gliding toward the target during a wind-upMode, ApproachTargetName (Target), StopDistanceOverride (cm between the bodies)
SEC Gameplay EventUSECGameplayEventNotifySending a gameplay event at a frameEventTag
SEC Anim Effect Window, SEC Play Anim EffectUSECAnimEffectWindowNotifyState, USECAnimEffectNotifyTrails, footsteps, foleyEffect
SEC has no combo notify: combos come from ChainLinks on the Action Set.

Gameplay tags you set

TagUse
SEC.Damage.Physical.Slashing, .Piercing, .Bludgeoning; SEC.Damage.Elemental.Fire, .Ice, .LightningDamageTags on a damage config
SEC.Stimulus.DamageTakenTrigger rule for a flinch
SEC.Stimulus.AttackTelegraphedTrigger rule for a parry or dodge
SEC.Role.Attacker, .Flanker, .Waiter, .Supporter, .EliteRole settings and per-role sets
SEC.Token.AttackCombat Token Gate
SEC.State.Intangiblei-frames in a State Tag Window
SEC.State.IsMovementBlockedAlready in each new action's AddTags
SEC.State.ReactionActiveStops two reactions overlapping (AddTags on one, BlockTags on the other)
SEC.Action.BlockActionsOn the ability system, stops all actions
SEC.Vital.Health, SEC.Vital.Stamina, SEC.Vital.StanceVitals rows

Common mistakes

SymptomCause and fix
Enemy stands still, no overlay above itThe pawn's AI controller is not an SEC one, or AIConfig is empty.
Enemy never notices the playerAwarenessConfig empty, or the player is on the enemy's team (Team Id 1).
Enemy moves but never attacksNo action passes its gates: bEnabled off, Selection not Scored, or a stuck SEC.Action.BlockActions tag. SEC.Debug.Scoring 1 prints the refusal reason.
Enemy swings from across the room or repeats one moveThe action has no Distance Scorer.
Action wins and nothing playsExecutionMethod empty, payload Montage empty, or ActionId empty.
Attacks once, then freezesThe ability never reaches End Ability. Use GA_SEC_MontageAbility, or set AbilityTimeout on the execution method.
Hits pass throughSocketIDs on the window do not match the weapon's TraceSockets ID, bUseAsWeapon is off, or the window sits on the wrong frames. The log names an unregistered socket ID.
Enemies hit each otherThe pawn's melee trace TeamFilter is NoFilter, the default on a fresh Blueprint. Set it to unreal.SECTeamFilter.ENEMIES_ONLY; the demo duplicate already does.
Never parriesThe attacker's montage has no SEC Telegraph Window, its Broadcast.Shape is empty, or the Reaction Set has no trigger rule.
Never flinchesThe pawn does not report damage (see Recipe 1, Step 7), or the set has no rule on SEC.Stimulus.DamageTaken.
Does not move at allNo navmesh, or a leftover SEC.State.IsMovementBlocked tag.
T-pose or no animationThe Animation Blueprint is for another skeleton, or has no DefaultSlot Slot node, so montages play nowhere.
Debug commands in Play: SEC.Debug.All 1, SEC.Debug.Awareness 1, SEC.Debug.Scoring 1, SEC.Debug.Melee.DrawTracing 1, SEC.Debug.LogActionDecisions 1, SEC.Debug.LogReactions 1, SEC.Debug.Stimulus.Draw 1. SEC.Debug.WatchPawn <name> narrows the overlays to one enemy.

Learn more

When the customer wants to understand something rather than build it, find the page in https://rigbak.com/docs/soulslike-combat/llms.txt, which lists every docs page with a one-line summary, fetch the page's Markdown version (the page URL plus .md), answer from it, and give them the page link. The table names the main pages.
TopicPage
Overviewhttps://rigbak.com/docs/SoulslikeCombatDocs
First enemyhttps://rigbak.com/docs/soulslike-combat/getting-started
Full enemy walkthroughhttps://rigbak.com/docs/soulslike-combat/step-by-step-enemy
Smallest possible enemyhttps://rigbak.com/docs/soulslike-combat/minimal-enemy
Actions, scoring, comboshttps://rigbak.com/docs/soulslike-combat/action-system
Action Set editorhttps://rigbak.com/docs/soulslike-combat/action-set-editor
Reactions and trigger ruleshttps://rigbak.com/docs/soulslike-combat/reaction-system
Reaction Set editorhttps://rigbak.com/docs/soulslike-combat/reaction-set-editor
Montages, notifies, root motionhttps://rigbak.com/docs/soulslike-combat/animation-integration
Weaponshttps://rigbak.com/docs/soulslike-combat/weapons
Hit detectionhttps://rigbak.com/docs/soulslike-combat/melee-trace
Blocks, parries, damage answershttps://rigbak.com/docs/soulslike-combat/defense-system
Health and staminahttps://rigbak.com/docs/soulslike-combat/vitals
Telegraphshttps://rigbak.com/docs/soulslike-combat/attack-telegraphs
Sight, hearing, stealthhttps://rigbak.com/docs/soulslike-combat/awareness-system
Movement and spacinghttps://rigbak.com/docs/soulslike-combat/movement-system
Targets and teamshttps://rigbak.com/docs/soulslike-combat/targeting-system
Combat roleshttps://rigbak.com/docs/soulslike-combat/combat-roles
Taking turnshttps://rigbak.com/docs/soulslike-combat/combat-tokens
Patrol posts and leasheshttps://rigbak.com/docs/soulslike-combat/territory
StateTree taskshttps://rigbak.com/docs/soulslike-combat/statetree
Every settinghttps://rigbak.com/docs/soulslike-combat/configuration
Recipes (bosses, packs)https://rigbak.com/docs/soulslike-combat/patterns
Multiplayerhttps://rigbak.com/docs/soulslike-combat/multiplayer
Troubleshootinghttps://rigbak.com/docs/soulslike-combat/troubleshooting
Blueprint and C++ APIhttps://rigbak.com/docs/soulslike-combat/api-reference

Reference files

The guide's reference files ship beside it, in the plugin's Resources/AgentGuide/ folder. Each follows under a heading naming its file.

Actions: the moveset in full (AgentGuide/Actions.md)

AGENTS.md, Recipe 1, Step 6, builds a two-attack Action Set with the helpers montage_method, range_scorer and action. This file covers everything else an action can say. Define those three helpers first (copy them from Step 6).
Tasks: a moveset from several animations; attacks for close, middle and long range; a combo that always follows on; a back-step or a gap closer; attacks only from the front, only at low health or only with stamina; pacing between attacks; several enemies taking turns; one long animation holding several attacks; a different moveset per weapon or per combat role; using the customer's own ability.
Recipe: a moveset from five animations
Two quick slashes that combo, an overhead slam, a thrust from farther away, and a back-step when the player is too close.
names = {"Slash1": "Attack_Slash_01", "Slash2": "Attack_Slash_02", "Slam": "Attack_Overhead", "Thrust": "Attack_Thrust", "BackStep": "Step_Back"}
am = {k: sec.create_montage_from_animation(load(f"/Game/.../{v}"), F, f"AM_{N}_{k}") for k, v in names.items()}
 
for key in ("Slash1", "Slash2", "Slam", "Thrust"):        # the back-step deals no damage and needs no window
    m, L = am[key], length(am[key])
    a = sec.add_notify_state_to_montage(m, unreal.SECApproachWindowNotifyState, L * 0.10, L * 0.20)
    a.set_editor_property("StopDistanceOverride", 40.0)
    w = sec.add_notify_state_to_montage(m, unreal.SECMeleeTraceNotifyState, L * 0.30, L * 0.25)
    w.set_editor_property("SocketIDs", ["Blade"])
    w.set_editor_property("DamageConfig", dmg_heavy if key == "Slam" else dmg_light)
    save(m)
 
aset = new_data_asset(unreal.ActionSet, F, f"ActionSet_{N}")
slash1 = action(aset, "Slash1", am["Slash1"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 80, 250, 400)], cooldown=2.0)
link = unreal.ActionChainLink()
link.set_editor_property("TargetActionId", "Slash2")
link.set_editor_property("BonusMultiplier", 1.5)
link.set_editor_property("Pacing", unreal.SECChainPacing.IMMEDIATE)
slash1.set_editor_property("ChainLinks", [link])
slash2 = action(aset, "Slash2", am["Slash2"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 80, 250, 400)])
slash2.set_editor_property("Selection", unreal.SECActionSelection.CHAIN_ONLY)
slam = action(aset, "Slam", am["Slam"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 100, 250, 350)], cooldown=6.0, weight=0.7, risk=1.5)
thrust = action(aset, "Thrust", am["Thrust"], [range_scorer(aset, unreal.SECDistanceScorer, 250, 350, 550, 700)], cooldown=4.0)
back = action(aset, "BackStep", am["BackStep"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 0, 100, 150)], cooldown=5.0, weight=0.8)
aset.set_editor_property("Actions", [slash1, slash2, slam, thrust, back])
save(aset)
  • Each band is a Distance Scorer: the action scores 1 between OptimalMin and OptimalMax and 0 outside MinValue to MaxValue (cm). Give each attack its own band; the thrust's band starts where the slashes end.
  • CHAIN_ONLY makes Slash2 play only after Slash1; IMMEDIATE pacing lets it start as soon as Slash1 ends, skipping the recovery gap.
  • The back-step's band ends at 150 cm, so it only plays with the player close. It has no approach window, so it steps back from where the enemy stands.
  • Point the AI Config's DefaultActionSet at the new set and save the config.
Check: reload the set and print each action's ActionId, its execution method's payload Montage, the band of its first scorer, and Slash1's chain link. sec.validate_asset(aset) returns nothing. In Play with SEC.Debug.Scoring 1, the overlay lists every action's score; SEC.Debug.LogActionDecisions 1 logs each pick, and Slash2 follows Slash1.
An action's fields (unreal.ActionSpec)
FieldDoes
ActionIdUnique name in the set; chain links name it. An empty one is dropped.
bEnabledOff keeps the action in the set but never picks it.
Selection (unreal.SECActionSelection)SCORED (picked by score), CHAIN_ONLY (only as a chain follow-up), MANUAL_ONLY (only when Blueprint or a State Tree runs it by name)
ExecutionMethodHow it runs (below).
SelectionWeight, RiskPenaltyMultiply and divide the score: below 1 and above 1 make it rarer.
CustomScoring (unreal.SECCustomScoring)Scorers (soft factors) and Gates (hard vetoes), below.
TagScoreMultipliersTag to multiplier: while the target holds the tag, the score is multiplied.
RequiresTags, BlockTagsTags that must be, or must not be, on the target or in the world.
bRequireLineOfSightOnly with a clear line to the target.
AddTagsTags the enemy holds while it runs. SEC.State.IsMovementBlocked is there from the start; leave it on attacks.
Cooldown (unreal.ActionCooldown)Duration, InitialCooldown (wait before the first use), Randomization (0 to 0.5), InterruptRefund (0 to 1), SpawnCooldownChance, MaxConsecutiveUses (-1 unlimited)
bOverrideRecoveryTime, RecoveryTimeThis action's own pause after it ends (else the AI Config's ActionRecoveryTime).
ChainLinks (unreal.ActionChainLink)TargetActionId, BonusMultiplier (score bonus while the chain window is open), Pacing (SCORE_ONLY, IMMEDIATE)
HookA USECActionHook Blueprint run around the action (a Blueprint step; duplicate BP_SEC_ExampleHook).
On the AI Config: ActionRecoveryTime, InterruptRecoveryTime, ActionRecoveryTimeRandomization (seconds the enemy waits after an action before picking another).
Execution methods and payloads
Class (picker label)RunsFields
USECAbilityExecutionMethod (Gameplay Ability)One ability and waits for it to endAbilityClass (GA_SEC_MontageAbility plays the payload's montage), AbilityTimeout, Payload
USECAbilityByTagExecutionMethod (Gameplay Ability By Tag)The granted ability carrying a tagActivationTag, AbilityTimeout, Payload
USECAbilityFireAndForgetExecutionMethod (Gameplay Ability (Fire And Forget))Starts an ability and ends the action at onceAbilityClass, ActivationTag
USECBehaviorTreeExecutionMethod (Behavior Tree Sequence)Behavior trees one after anotherAgentGuide/BehaviorTrees.md
The payload is a USECMontagePayload (Montage Payload): Montage, StartSection, PlayRate. Make it with the method as its outer (montage_method does). An ability the customer wrote must reach End Ability on every path, or the action never ends; AbilityTimeout is the safety limit.
One clip, several attacks. Give the montage sections, then set each payload's StartSection:
sec.set_montage_sections(am_combo, [("Slash", 0.0), ("Stab", 1.1), ("Spin", 2.3)])
A section with no next section named ends the montage, so each action plays only its part.
Scorers and gates
Class (picker label)InFields
USECDistanceScorer (Distance Scorer)ScorersRange (cm)
USECAngleScorer (Angle Scorer)ScorersRange (degrees off facing the target; default peaks at 0 to 30)
USECHealthScorer (Health Scorer)ScorersRange (own health, 0 to 1)
USECSpeedScorer (Speed Scorer)ScorersRange (own speed, cm/s)
USECVitalScorer (Vital Scorer)ScorersVitalTag, bUseFraction, Range
USECAttributeScorer (Attribute Scorer)ScorersAttribute, NormalizeBy, ValueEval
USECStaminaGate (Stamina Gate)GatesMinStamina
USECVitalGate (Vital Gate)GatesVitalTag, MinValue, bUseFraction
USECTokenGate (Combat Token Gate)GatesTokenTag (SEC.Token.Attack), Cost
Every gate has bInvert. A Range (unreal.RangeEval) has MinValue, OptimalMin, OptimalMax, MaxValue, Exponent (falloff sharpness) and bClampToZero.
Phases by health: a Health Scorer with Range 0.49, 0.5, 1.0, 1.0 on phase one actions and 0.0, 0.0, 0.5, 0.51 on phase two (AGENTS.md, Recipe 5).
Taking turns: a Combat Token Gate on each attack; how many slots a target has is in AgentGuide/BrainRolesTargets.md.
Movesets per weapon and per role
  • On a weapon Blueprint's class defaults, WeaponActionSet (and WeaponReactionSet) win over the AI Config while it is equipped.
  • On the AI Config, RoleActionSets takes unreal.RoleActionSetConfig rows (Role, bManaged, ActionSet); roles not listed use DefaultActionSet. bManageActionSetsAutomatically (on) is the switch for both.
Mistakes
SymptomCause and fix
One attack fires at every rangeIt has no Distance Scorer.
The follow-up never playsIts ActionId differs from the link's TargetActionId, or its own band leaves it at 0 where the first attack ends.
The enemy spams one moveNo Cooldown.Duration, or every other action scores 0 at that range. SEC.Debug.Scoring 1 shows why.
An action wins and nothing playsEmpty ExecutionMethod, payload Montage or ActionId. A whole-array write does not fill the method; make it yourself.
The back-step plays at rangeIts band reaches too far; keep MaxValue near the reach of a player's swing.

Animation: montages, windows and the warp (AgentGuide/Animation.md)

AGENTS.md, Recipe 1, Steps 2 and 4, makes montages and puts an approach window and a trace window on each attack. This file covers every window SEC reads, the skew warp for root-motion lunges, sections, tag-driven animation bools and the speed modifier.
Tasks: an attack that turns and closes the gap; a step back or to the side by a set distance; a lunge whose root motion tracks the target; i-frames on a dodge; an event at a frame; footsteps and trails; several attacks in one clip; a guard pose while a tag is held; walking slower while blocking.
Windows SEC reads
Add each with sec.add_notify_state_to_montage(montage, cls, start_seconds, duration_seconds), set its properties on the returned window, and save the montage. sec.add_notify_state_to_montage takes notify states only; a single-frame notify is added with unreal.AnimationLibrary.add_animation_notify_event(montage, "Combat", seconds, cls).
Window (editor name)ClassFields
SEC Melee Trace WindowUSECMeleeTraceNotifyStateSocketIDs (weapon trace socket IDs; empty sweeps all), DamageConfig, ExtraDamageTags
SEC Approach WindowUSECApproachWindowNotifyStateMode (unreal.SECApproachMode: MOVE_AND_FACE, FACE_ONLY, MOVE_ONLY), ApproachTargetName (Target), StopDistanceOverride (cm left between the bodies), MaxApproachDistanceOverride; bMoveByOffset with OffsetDirection and OffsetDistance moves a set distance in the enemy's own frame instead (a back-step: direction (-1, 0, 0), 200 cm)
SEC State Tag WindowUSECStateTagNotifyStateTags held while it runs: SEC.State.Intangible (i-frames), SEC.State.Blocking
SEC Telegraph WindowUSECTelegraphNotifyStateBroadcast (unreal.SECTelegraphBroadcast: Shape = USECTelegraphShape_Sphere Radius or USECTelegraphShape_Cone Radius, HalfAngleDegrees), bFindAttackAutomatically, DamageConfig (shown as Attack), Tags, Magnitude
SEC Anim Effect WindowUSECAnimEffectWindowNotifyStateEffect (EffectTags, SocketName), answered by an anim effect handler on the pawn
SEC Gameplay Event (one frame)USECGameplayEventNotifyEventTag
SEC Play Anim Effect (one frame)USECAnimEffectNotifyEffect
Root-motion lunges: the skew warp
An attack animated with root motion that should land on the target (rather than glide by an approach window) uses the engine's Motion Warping window with the SEC Skew Warp modifier:
w = sec.add_notify_state_to_montage(am_lunge, unreal.AnimNotifyState_MotionWarping, L * 0.1, L * 0.3)
mod = unreal.new_object(unreal.SECRootMotionModifier_SkewWarp, outer=w)
mod.set_editor_property("WarpTargetName", "Target")
mod.set_editor_property("StopDistanceOverride", 40.0)
w.set_editor_property("RootMotionModifier", mod)
save(am_lunge)
The demo enemy carries the Motion Warping component it needs. SEC.Debug.Warp 1 draws where each warp lands.
Sections
sec.set_montage_sections(montage, [("Slash", 0.0), ("Stab", 1.1)]) gives a montage sections; a Montage Payload's StartSection then plays one (AgentGuide/Actions.md).
Animation Blueprint
  • The locomotion template (sec.create_locomotion_anim_blueprint, AGENTS.md, "No Animation Blueprint") blends idle, walk and run and plays montages on DefaultSlot.
  • Tag-driven bools: an Animation Blueprint whose parent is USECAnimInstanceBase has GameplayTagPropertyMap on its class defaults: each row sets a bool variable while the enemy holds a tag (a guard pose while SEC.State.Blocking). Wiring that bool into the AnimGraph is a step for the customer.
Walking slower while a tag is held
Add a SEC Speed Modifier component (USECSpeedModifierComponent) to the enemy Blueprint (AgentGuide/Python.md, Components on a Blueprint) and fill SpeedScaleByTag, such as {tag("SEC.State.Blocking"): 0.5}.
Mistakes
SymptomCause and fix
Swings short of the targetNo approach window, or StopDistanceOverride left at -1 (the approach component's 150).
The lunge overshootsThe Motion Warping window has no modifier, or its WarpTargetName is not Target.
The dodge does not avoid hitsNo SEC.State.Intangible window over the evasive frames.
Nothing happens on an effect windowThe pawn has no anim effect handler; that is a Blueprint step.

Awareness and territory: noticing, patrols and leashes (AgentGuide/AwarenessTerritory.md)

Tasks: how far and how fast an enemy notices the player; a guard that notices slowly from the side; an enemy that knows about the player at once (a boss); waking nearby allies; patrolling between points; guarding one spot; giving up a chase past a distance and walking back.
Awareness
An enemy notices targets through its Awareness Config (USECAwarenessConfig), named on the AI Config as AwarenessConfig. Without one the enemy never notices anyone. The demo's is /SoulslikeEnemyCombat/Demo/AI/Awareness_SEC_Default.
FieldDoes
SightRadius, LoseSightRadiusSight range in cm, and the range at which a seen target is lost
PeripheralVisionHalfAngleHalf the sight cone, in degrees (90 is a 180 degree cone)
bEnableHearing, HearingRangeHearing and its range in cm
bEnableDamageSenseBeing hit reveals the attacker
bGradualEscalation, EscalationRate, SuspicionThresholdSight fills a 0 to 1 meter, faster up close and straight ahead, before the enemy fully notices; the threshold is where it turns suspicious
ProximityRadiusInside this many cm a target stays noticed without sight
GraceLossDurationSeconds it keeps fighting after losing sight
SuspiciousDecayDurationSeconds a full meter takes to drain
ConfirmLostDelaySeconds without sight before a noticed target counts as lost
bAlwaysAwareKnows every hostile target at once, no sight needed (a boss in its arena)
bAlertAlliesOnDetect, AllyAlertRadius, AllyAlertCooldownTells nearby allies when it notices someone
A slow, side-blind guard:
aw = new_data_asset(unreal.SECAwarenessConfig, F, f"Awareness_{N}_Guard")
aw.set_editor_property("SightRadius", 1800.0)
aw.set_editor_property("PeripheralVisionHalfAngle", 60.0)
aw.set_editor_property("bGradualEscalation", True)
aw.set_editor_property("EscalationRate", 0.5)
aw.set_editor_property("bAlertAlliesOnDetect", True)
save(aw)
config.set_editor_property("AwarenessConfig", aw)
save(config)
Check: in Play, SEC.Debug.All 1 writes the awareness state (Suspicious, Detected) above the enemy as the player walks into view.
The target selector turns what the enemy noticed into its target: unreal.AwarenessFilteredTargetSelector (MinRequiredState, InnerSelector) on the AI Config, as AGENTS.md, Recipe 1, Step 8, sets. Sight skips actors on the enemy's own team (AgentGuide/BrainRolesTargets.md, Teams).
Territory: patrols, guard posts and leashes
Every enemy on AEnemyCharacterBase carries a SEC Territory component, TerritoryComponent. It is set on the placed enemy in the level, since posts are level actors. Posts are SEC Territory Post actors (ASECTerritoryPost), each tuned on its Post component (USECTerritoryPostComponent).
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
posts = [actors.spawn_actor_from_class(unreal.SECTerritoryPost, unreal.Vector(x, y, 50)) for x, y in ((0, 0), (1200, 0), (1200, 900))]
for p in posts:
    pc = p.get_editor_property("Post")
    pc.set_editor_property("WaitTime", 2.0)
    pc.set_editor_property("bFaceForwardOnArrival", True)
 
terr = placed_enemy.get_editor_property("TerritoryComponent")
terr.set_editor_property("Posts", posts)
terr.set_editor_property("RestingBehavior", unreal.SECRestingBehavior.PATROL_POSTS)
terr.set_editor_property("PostOrder", unreal.SECPostOrder.SEQUENTIAL_LOOP)
terr.set_editor_property("bLeashEnabled", True)
terr.set_editor_property("LeashAnchor", unreal.SECLeashAnchor.POSTS_CENTERED)
terr.set_editor_property("LeashRadius", 1500.0)
terr.set_editor_property("DisengageBehavior", unreal.SECDisengageBehavior.RETURN_TO_POST)
unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
placed_enemy is the enemy actor in the level (AGENTS.md, Recipe 1, Step 9, keeps it as enemy).
SEC Territory fieldDoes
PostsThe posts this enemy owns, in route order
RestingBehaviorPATROL_POSTS (walk the route) or HOLD_GROUND (stand at the first post)
PostOrderSEQUENTIAL_LOOP, SEQUENTIAL_PING_PONG, RANDOM
PatrolStartDelay, PatrolStartVariance, ArrivalRadiusWhen it sets off, and how close counts as arrived
bLeashEnabled, LeashAnchor (SELF, POSTS_CENTERED, POSTS_UNION, ACTOR with LeashAnchorActor), LeashRadiusHow far it may be dragged from its ground
LeashBreakGraceTimeSeconds past the leash before it gives up
bOnlyFightInsideLeashIgnore targets outside the leash
DisengageBehaviorRETURN_TO_POST or SETTLE_HERE once a fight ends
Post fieldDoes
ShapePOINT, RADIUS (wander inside WanderRadius), BOX (inside WanderBoxExtent)
WaitTime, WaitTimeVarianceSeconds it waits on arrival
bFaceForwardOnArrivalTurn to the post's facing
bPassThroughWalk through without stopping
The built-in brain (the AI Config's StateTree, shown as Default State Tree, left empty) walks the posts, goes to look where it last sensed the player, and walks home after a fight. The demo brain /SoulslikeEnemyCombat/Demo/AI/StateTrees/ST_SEC_Core runs patrol, investigation and the walk home as a State Tree the customer can open and change; set it as the AI Config's StateTree only when they want that.
Check: in Play, the enemy walks its posts; dragged past LeashRadius for LeashBreakGraceTime it turns back to its posts.
Mistakes
SymptomCause and fix
Never notices the playerAwarenessConfig empty, the player beyond SightRadius, outside the cone, or on the enemy's team.
Notices through wallsProximityRadius large, or hearing on with a loud level.
Stands still instead of patrollingPosts empty, RestingBehavior on HOLD_GROUND, no navmesh between the posts, or a StateTree without the territory tasks.
Chases foreverbLeashEnabled off, or the leash anchor is SELF with bTrackLeashAnchor on.
The patrol is lost after a restartThe territory was set on the Blueprint rather than the placed enemy, or the level was not saved.

Behavior trees and State Trees (AgentGuide/BehaviorTrees.md)

Tasks: run one attack as a behavior tree with the plugin's tasks (close in, swing, back off); tune the shipped tree; know what each State Tree task does.
An action that runs a behavior tree
An action's ExecutionMethod can be a Behavior Tree Sequence (USECBehaviorTreeExecutionMethod): it runs its trees one after another on the controller, then the action ends.
FieldDoes
AbilityToGrantThe gameplay ability the trees start (GA_SEC_MontageAbility plays the payload's montage)
BehaviorTreeSequenceThe trees, in order
BehaviorTreeTimeoutSeconds before a tree is stopped
PayloadKeyNameBlackboard key the payload is written to, SEC_Payload
PayloadA USECMontagePayload (outer: the method), as for an ability method
The plugin's tasks, which the editor's behavior tree palette lists:
Task (class)DoesFields
Move Until Distance (UBTTask_MoveUntilDistance)Walks toward the blackboard target until within reachTargetDistance (cm), MaxChaseDistance, TimeoutSeconds, bAllowStrafe, bUsePathfinding, bAllowPartialPath
Activate Action Ability (UBTTask_ActivateActionAbility)Starts the ability the running action named and waits for it to endFallbackActivationTag, bFailWhenNoAbility
Activate Ability (UBTTask_ActivateAbility)Starts a named abilityAbilityClass, ActivationMode (unreal.AbilityActivationMode.BY_TAG, BY_EVENT), EventTag (shown as Activation Tag), bGrantAbilityIfMissing
Make Distance (UBTTask_MakeDistance)Backs away from the target without pathfindingTargetDistance (cm), MovementSpeed (0 to 1), Timeout, StuckTimeout
Blackboard keys the method writes: SEC_TargetActor, SEC_SelfActor, SEC_Distance, SEC_ActionId, SEC_Payload. The shipped blackboard BB_SEC_Basic holds SEC_TargetActor, SEC_Distance, SEC_ActionId and SEC_Payload.
Recipe: close in, swing, back off
Editor Python cannot build a behavior tree's shape. The shipped BT_SEC_AbilityAction already is this shape (a Sequence of Move Until Distance, Activate Action Ability and Make Distance, with a Wait), so duplicate it and tune its tasks:
bt = duplicate("/SoulslikeEnemyCombat/Demo/AI/BehaviorTrees/BT_SEC_AbilityAction", F, f"BT_{N}_HeavyAttack")
pkg = bt.get_outermost().get_name()
nodes = [o for o in unreal.ObjectIterator(unreal.BTNode) if o.get_outermost().get_name() == pkg]
for n in nodes:
    kind = n.get_class().get_name()      # the plugin's task classes have no Python type of their own; match them by name
    if kind == "BTTask_MoveUntilDistance":
        n.set_editor_property("TargetDistance", 150.0)
    elif kind == "BTTask_MakeDistance":
        n.set_editor_property("TargetDistance", 450.0)
save(bt)
 
method = unreal.new_object(unreal.SECBehaviorTreeExecutionMethod, outer=aset)
method.set_editor_property("AbilityToGrant", bp_class("/SoulslikeEnemyCombat/Core/Abilities/GA_SEC_MontageAbility"))
method.set_editor_property("BehaviorTreeSequence", [bt])
payload = unreal.new_object(unreal.SECMontagePayload, outer=method)
payload.set_editor_property("Montage", am_heavy)
method.set_editor_property("Payload", payload)
Use method as the action's ExecutionMethod in place of montage_method(...), then write the actions back and save the set. Keep a Distance Scorer on the action.
Check: reload the tree and print each task's TargetDistance; in Play, SEC.Debug.LogActionExecution 1 logs the tree starting and ending with the action.
A tree of another shape (a Selector, decorators, other tasks) is built by hand in the Behavior Tree editor: tell the customer which tasks in which order, from the table above.
State Trees
The brain is a State Tree when the AI Config's StateTree names one; editor Python cannot build or edit a State Tree's states. Use the shipped trees (ST_SEC_Core: notice, patrol, investigate, fight, walk home; ST_SEC_RoleBehavior: per combat role) or have the customer duplicate one and edit it in the State Tree editor. The plugin's tasks and conditions there:
TaskDoes
Build Decision ContextGathers distance, angle, health and threat each tick
Poll Action, Do ActionScores the actions and runs the chosen one
AI MovementRuns the movement profile
Poll Combat Role, Poll Focus Actor, Poll Last Known LocationRead the role, the focus target, where the target was last seen
Sync Action Set, Sync Reaction Set, Sync Movement Profile, UpdateActionSetSwap sets on a role change, or to a named Action Set
Territory Patrol, Territory Investigate, Return To Territory, Hold Out Of Combat, Monitor Combat LeashPatrol, search, walk home, stand down, watch the leash
ConditionPasses when
Awareness State At LeastThe target is noticed to a level
Target Eligible For Combat, Target Outside Leash, Pawn Outside Leash, Pawn On Its Ground, Territory Owns Ground, Resting Behavior IsTerritory and leash state
Action Executing, Action Recovering, Actions Blocked, Reaction Executing, ASC Has TagCombat state
Mistakes
SymptomCause and fix
The action never endsA tree waits forever: set BehaviorTreeTimeout, and check the ability reaches End Ability.
Activate Action Ability failsAbilityToGrant is empty, so the enemy holds no ability to start.
Move Until Distance never arrivesNo navmesh with bUsePathfinding on, or TargetDistance inside the target's capsule.

Brain, combat roles, targets, teams and turn-taking (AgentGuide/BrainRolesTargets.md)

Tasks: which target an enemy picks when there are several players; enemies spreading across players; an enemy that only ever flanks; at most two attackers at once; enemies taking turns on one player; factions that fight each other; a fight that repeats exactly; choosing the brain.
All of it sits on the Enemy AI Config (UEnemyAIConfig), the pawn's AIConfig, plus a few Project Settings.
The AI Config
FieldDoes
TargetSelectorHow it picks a target (below). Empty picks the first registered target it may fight.
TargetFilterWhich actors it will fight at all; empty uses the built-in rule (never itself, never an ally, never an excluded team). A custom filter is a Blueprint child of USECTargetFilter (a Blueprint step).
bAutoRegisterForCombatRoles, RoleRegistrationParams (unreal.AIRoleRegistrationParams)Combat roles (below).
bIgnoreTargetRedistributionKeep its target when targets are shared out again.
DefaultActionSet, RoleActionSets; DefaultReactionSet, RoleReactionSets; DefaultMovementProfile, RoleMovementProfilesWhat it fights, reacts and moves with, per role.
AwarenessConfig, StimulusFiltersWhat it notices (AgentGuide/AwarenessTerritory.md, AgentGuide/Reactions.md).
StateTree (shown as Default State Tree)Empty runs the built-in brain: notice, investigate, close in, circle, attack, react, plus patrol and the walk home once the placed enemy's territory has posts. ST_SEC_Core runs the same phases as a State Tree the customer can edit; ST_SEC_RoleBehavior drives each combat role differently. Both under /SoulslikeEnemyCombat/Demo/AI/StateTrees/.
DecisionContextParamsAggressionLevel (0 to 2) and line-of-sight trace settings.
ActionRecoveryTime, InterruptRecoveryTime, ActionRecoveryTimeRandomizationPause after each action (AgentGuide/Actions.md).
bDeterministic, DefaultSeed, StartBarrierSecondsThe same picks every run, for a boss fight that repeats exactly; SEC.Determinism.Trace and SEC.Determinism.Compare check it.
Target selection
Instanced: make it with the config as outer.
Class (picker label)Picks
UAwarenessFilteredTargetSelector (Awareness Filtered Target Selector)Only targets the enemy has noticed (MinRequiredState, unreal.SECAwarenessState), then asks its InnerSelector
UFirstTargetSelector (First Target Selector)The first registered target
UClosestTargetSelector (Closest Target Selector)The nearest
URandomTargetSelector (Random Target Selector)A random one, spreading enemies
UBalancedTargetSelector (Balanced Target Selector)The target with the fewest enemies on it
UFixedTargetSelector (Fixed Target Selector)The actor carrying the actor tag RequiredTag
Spread a pack over two players:
sel = unreal.new_object(unreal.AwarenessFilteredTargetSelector, outer=config)
sel.set_editor_property("InnerSelector", unreal.new_object(unreal.BalancedTargetSelector, outer=config))
config.set_editor_property("TargetSelector", sel)
save(config)
Combat roles
Roles share out what each enemy does in a group fight: SEC.Role.Attacker, .Flanker, .Waiter, .Supporter, .Elite. RoleRegistrationParams on the config: AllowedRoles (empty allows any), PreferredRole, Priority (0 to 1000, higher picks first), FitnessEvaluators (UDistanceRoleEvaluator IdealDistance, EffectiveRange; UCooldownRoleEvaluator CurrentRolePenaltyMultiplier; each with RoleWeights, a map of role tag to weight).
p = config.get_editor_property("RoleRegistrationParams")
p.set_editor_property("AllowedRoles", tags("SEC.Role.Flanker"))
p.set_editor_property("PreferredRole", tag("SEC.Role.Flanker"))
config.set_editor_property("RoleRegistrationParams", p)
save(config)
How many at once is a Project Setting: RoleLimits on unreal.SoulslikeEnemyCombatSettings (unreal.RoleLimitConfig rows Role, Limit), with RoleReassignmentInterval and MinTimeInRole. A project setting changed from Python lasts until the editor closes (AgentGuide/Python.md, Project Settings), so ask the customer to set it in Project Settings, Plugins, Soulslike Enemy Combat, Combat Roles. Elite slots are unlimited.
Check: in Play, SEC.Debug.LogRoleAssignments 1 logs each assignment; the overlay from SEC.Debug.All 1 names each enemy's role.
Taking turns on one target
Put a Combat Token Gate (USECTokenGate, TokenTag SEC.Token.Attack, Cost 1) in each attack's CustomScoring.Gates (AgentGuide/Actions.md). How many enemies may hold SEC.Token.Attack against one target is TokenBudgets in Project Settings (unreal.SECTokenBudgetConfig rows TokenTag, Budget, ReissueDelay), or per target with a SEC Token Budget Component (USECTokenBudgetComponent) on the player Blueprint: add it as AgentGuide/Python.md, Components on a Blueprint, shows, set its TokenBudgets to {tag("SEC.Token.Attack"): 1}, compile and save.
Teams
Every actor's team is its SEC Team component (USECTeamComponent): TeamId (a number; enemies default to 1, the player is 0), bFreeForAll, ExcludedTeamIds, AllowedHostileTeamIds, bIgnorePlayer, bIgnoreNeutrals, FightOnly (one actor and nothing else). An actor with no team component is neutral and can be fought. Enemies on AEnemyCharacterBase carry one as TeamComponent. Two factions: give each enemy Blueprint its own TeamId. The melee trace's TeamFilter decides whether a swing can hit allies. SEC.Debug.All 1 shows each actor's team.
Mistakes
SymptomCause and fix
Every enemy goes for the same playerNo selector, or a First or Closest selector; use Balanced or Random inside the awareness filter.
A role is never assignedbAutoRegisterForCombatRoles off, the role is outside AllowedRoles, or its limit is full.
Enemies fight each otherThey share no team and are on different TeamIds, or bFreeForAll is on.
Two enemies attack at once despite the token gateThe budget is above 1, or one attack lacks the gate.

Movement: spacing, circling and backing off (AgentGuide/Movement.md)

Tasks: how far an enemy keeps from its target; circling and how often it switches direction; backing off when the player stares at it; flanking from an angle; different spacing per combat role; stopping farther or closer when an attack closes the gap.
Everything here is data. An enemy moves by its Movement Behavior Profile (UMovementBehaviorProfile), named on its AI Config as DefaultMovementProfile (AGENTS.md, Recipe 1, Step 8).
Recipe: a cautious duelist
Keeps about four metres away, circles the player, and backs off while the player looks at it.
movement = new_data_asset(unreal.MovementBehaviorProfile, F, f"Movement_{N}_Duelist")
movement.set_editor_property("DesiredDistance", 400.0)          # cm between the enemy and its target
movement.set_editor_property("DistanceTolerance", 0.15)
movement.set_editor_property("CrowdTolerance", 0.2)            # steps back once closer than 320 cm
movement.set_editor_property("bEnableStrafing", True)
movement.set_editor_property("StrafeTimeLimit", 4.0)
movement.set_editor_property("bAdjustDistanceByThreat", True)
movement.set_editor_property("ThreatDistanceScale", 0.5)        # up to 1.5 times the distance while stared at
movement.set_editor_property("bSwapStrafeOnHighThreat", True)
save(movement)
 
config = load(f"{F}/EnemyConfig_{N}")
config.set_editor_property("DefaultMovementProfile", movement)
config.set_editor_property("ActionRecoveryTime", 2.5)          # seconds between attacks, spent backing off and circling
save(config)
 
aset = config.get_editor_property("DefaultActionSet")
bands = [s for spec in aset.get_editor_property("Actions")
         for s in spec.get_editor_property("CustomScoring").get_editor_property("Scorers")
         if isinstance(s, unreal.SECDistanceScorer)]
farthest = max(bands, key=lambda s: s.get_editor_property("Range").get_editor_property("MaxValue"))
r = farthest.get_editor_property("Range")
r.set_editor_property("MaxValue", max(r.get_editor_property("MaxValue"), 750.0))
farthest.set_editor_property("Range", r)
save(aset)
The profile only moves the enemy while no attack can start, so ActionRecoveryTime gives it that time after each attack; with none it attacks again as soon as an attack is ready and never backs off. CrowdTolerance 0.2 sends it back out past 320 cm after an attack; the default 0.45 lets it stand at 220 cm. While stared at, this enemy waits up to 400 x 1.15 x 1.5 = 690 cm out, past the 450 and 500 cm bands of the core recipe's attacks. An attack starts only where its Distance Scorer reaches ( below), so the last block stretches the band of the attack that reaches farthest to 750 cm, the farthest its approach window glides in. Without it the enemy circles at range and never attacks a player who keeps it on screen.
How long a stare takes to count is on the AI controller's Threat Detection component. The demo controller BP_SEC_EnemyController carries one; to change it, duplicate the controller into the customer's folder, set it on the pawn (AIControllerClass on the pawn's class defaults), and edit its class defaults:
ctrl = duplicate("/SoulslikeEnemyCombat/Demo/Characters/Enemy/BP_SEC_EnemyController", F, f"BP_{N}_Controller")
threat = defaults(ctrl).get_editor_property("ThreatDetectionComponent")
threat.set_editor_property("PlayerLookAngleThreshold", 20.0)   # degrees off the enemy that count as looking at it
threat.set_editor_property("PlayerLookTimeThreshold", 1.0)     # seconds of looking before it counts
save(ctrl)
defaults(bp).set_editor_property("AIControllerClass", ctrl.generated_class())
save(bp)
Check: reload the config and print its DefaultMovementProfile and the stretched band's MaxValue. In Play, SEC.Debug.All 1 shows the enemy's movement state; it stops at about DesiredDistance, circles, opens the gap while the player keeps it centred on screen (SEC.Debug.Threat.Draw 1 draws the look cone), and still attacks from there.
The profile's fields
FieldDefaultDoes
DesiredDistance400Distance in cm the enemy keeps from its target
DistanceTolerance0.2How far past DesiredDistance still counts as close enough, as a share of it (0.05 to 0.5)
CrowdTolerance0.45How far inside DesiredDistance the enemy tolerates before stepping back, as a share of it
bEnableStrafingtrueCircle the target once at the distance
StrafePreferenceWeight1How strongly it prefers circling over standing
bAutoStrafeSwap, StrafeSwapThreshold, StrafeSwapCooldowntrue, 3, 2Switch circling direction when the other side scores that many times better
bEnableStrafeTimeLimit, StrafeTimeLimit, StrafeTimeLimitFluctuationtrue, 5, 2.5Switch direction after circling one way this long (seconds, plus or minus the fluctuation)
bRestAfterStrafeSwap, StrafeSwapRestTime, StrafeSwapRestFluctuationfalse, 0.5, 0.25Pause after a timed switch
bEnableStrafeRest, StrafeRestTimeLimit, StrafeRestDurationtrue, 10, 1.5Stop and rest after circling this long
bAdjustDistanceByThreat, ThreatDistanceScalefalse, 1Multiply the distance by 1 + threat x scale; threat climbs from 0 to 1 while the player looks at the enemy
bSwapStrafeOnHighThreatfalseSwitch circling direction once the player has stared long enough
PositioningRulesemptyExtra direction preferences, below
Angle preference. UAnglePreferenceRule in PositioningRules pulls the enemy toward an angle off the target's facing: PreferredAngle (0 in front, 90 at the side, 180 behind; default 90), AngleTolerance (30), DirectionCommitment (45), bAvoidInstead (stay away from that angle), Weight (0 to 1). Make it with unreal.new_object(unreal.AnglePreferenceRule, outer=movement) and write the list back. A flanker uses PreferredAngle 120 to 150.
Per combat role. RoleMovementProfiles on the AI Config takes unreal.RoleMovementProfileConfig rows (Role a SEC.Role.* tag, MovementProfile); roles not listed use DefaultMovementProfile. The demo ships one profile per role under /SoulslikeEnemyCombat/Demo/AI/MovementProfiles/ (MovementProfile_SEC_Attacker, _Flanker, _Waiter, _Supporter, _Elite); duplicate one as a starting point.
Threat. UThreatDetectionComponent on the controller: PlayerLookAngleThreshold (15 degrees) and PlayerLookTimeThreshold (3 seconds). The controller's CombatControllerComponent turns it on (bEnableThreatDetection, on by default).
Getting in range to attack
The profile decides where the enemy waits between attacks; the enemy picks its next attack from there. An action can start only while the gap is inside its Distance Scorer's band (below MaxValue). The attack then closes the last gap through the SEC Approach Window on its montage (AGENTS.md, Recipe 1, Step 4): its StopDistanceOverride is the gap it leaves, and the pawn's SEC Approach component sets the rest: DefaultStopDistance, DefaultMaxApproachDistance (farthest it glides, 750 cm), SpeedLimit.
So the farthest the enemy waits, DesiredDistance x (1 + DistanceTolerance), times (1 + ThreatDistanceScale) while stared at when bAdjustDistanceByThreat is on, has to sit inside at least one attack's band.
Mistakes
SymptomCause and fix
The enemy hugs the playerDesiredDistance below about 150, or the profile is not the config's DefaultMovementProfile while bManageMovementProfilesAutomatically is on (the default).
It jitters back and forthDistanceTolerance too small for the distance; raise it toward 0.2.
It stays close after each attackAn attack is always ready, so it never has time to walk back. Set ActionRecoveryTime on the AI Config (the demo uses 2), or longer Cooldown.Duration on the attacks.
It circles but never attacksIt waits farther out than any attack's Distance Scorer reaches, often only while the player looks at it (bAdjustDistanceByThreat). Stretch the farthest attack's MaxValue past the farthest wait, as the recipe does, or lower DesiredDistance or ThreatDistanceScale.
It never backs off when stared atbAdjustDistanceByThreat off, or the controller has no Threat Detection component (the demo controller has one).
It changes direction constantlyStrafeTimeLimit short with bAutoStrafeSwap on; raise StrafeSwapThreshold or StrafeTimeLimit.
A role's profile is ignoredThe role tag on the row does not match the role the enemy holds; SEC.Debug.LogRoleAssignments 1 logs it.

Editor Python: what can be set, and how (AgentGuide/Python.md)

Rules every other file relies on. Read this when a set_editor_property call is refused, when a value you wrote is gone after a reload, or before you add components, rows or instanced objects.
Tasks: set a property; edit a row in an array; add an instanced object (an execution method, a scorer, a selector); edit a component on a Blueprint; give a montage sections; add a gameplay tag; check an asset; save a Project Settings value; read the log.
Setting properties
  • obj.set_editor_property("Name", value) takes the C++ name (bAlwaysAware) or the Python name (always_aware). It refuses a property the editor shows read-only, and a class-defaults-only property on anything that is not a template (an asset, a class default object, or an object inside an asset).
  • Arrays and their elements come back as copies. Edit the element, put it back in the list, and write the whole list:
    actions = list(aset.get_editor_property("Actions"))
    a = actions[0]
    a.set_editor_property("SelectionWeight", 0.8)
    actions[0] = a
    aset.set_editor_property("Actions", actions)
  • Nested structs (an action's Cooldown, a scorer's Range) too: read, edit, write back to the owner, as AGENTS.md's range_scorer does.
  • A whole-array write does not run the editor's own fill-ins: a new action written this way gets no execution method, so make one (montage_method).
Instanced objects
Execution methods, payloads, scorers, gates, target selectors, positioning rules, stimulus filters, hit effects and defense rules live inside the asset holding them.
  • Make one with unreal.new_object(cls, outer=<the asset that holds it>). A payload's outer is its execution method.
  • Never give a class default object as the outer (unreal.get_default_object(...)), not even to look at a class: the next object of that class with the same name takes it as its template, and the asset holding it fails to save.
Components on a Blueprint
  • A component the C++ base creates is read from the class defaults: on the pawn, defaults(bp).get_editor_property("MeleeTraceComponent") (also EquipmentComponent, Vitals, CharacterMovement, Mesh); on the controller, ThreatDetectionComponent, MovementEvaluatorComponent, ActionEvaluationComponent, AwarenessComponent, CombatControllerComponent, SECBrainComponent. Edit it there and save the Blueprint.
  • A component the Blueprint adds lives in its construction script; bp_component(bp, "<name start>") from AGENTS.md returns its template. Edit it, compile with unreal.BlueprintEditorLibrary.compile_blueprint(bp) and save.
  • Adding a component:
    sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)
    root = sds.k2_gather_subobject_data_for_blueprint(bp)[0]
    params = unreal.AddNewSubobjectParams(parent_handle=root, new_class=unreal.SECTerritoryComponent, blueprint_context=bp)
    handle, fail = sds.add_new_subobject(params)
Montage sections
The engine's Python cannot give a montage sections. Use the plugin's tool:
sec.set_montage_sections(montage, [("Slash", 0.0), ("Stab", 1.1), ("Spin", 2.3)])
It replaces every section and saves the montage. Each section ends the montage, so an action whose payload names a StartSection plays only that part. A third value names the section it plays on into instead, or its own name to loop it: ("Hold", 0.4, "Hold").
Gameplay tags
A tag must exist before a property can hold it. The plugin's role, token, stimulus, state and vital tags exist already. To add one, such as a role or a token pool of the project's own:
sec.add_gameplay_tag("SEC.Role.Guardian", "Stays beside the boss")
It writes Config/DefaultGameplayTags.ini and the tag can be used at once.
Project Settings
unreal.get_default_object(unreal.SoulslikeEnemyCombatSettings) edits the values under Project Settings, Plugins, Soulslike Enemy Combat (RoleLimits, TokenBudgets, DefaultTargetSelectorClass, DefaultTargetFilterClass, vital tags). Editor Python cannot write them to the project's config file before UE 5.8, so a change lasts until the editor closes: ask the customer to set the value in Project Settings, or on UE 5.8 save it with Epic's MCP ConfigSettingsToolset.
Checks
  • sec.validate_asset(asset) runs the checks the editor's Validate Assets runs (Action Sets, Reaction Sets, damage configs) and returns one line per problem.
  • After Play, log_warnings() returns SEC's warnings; each names the asset and the fix.

Reactions: flinches, parries, dodges and counters (AgentGuide/Reactions.md)

AGENTS.md, Recipe 1, Step 7, makes a flinch and Recipe 4 a parry. This file covers the rest of a Reaction Set.
Tasks: flinch only sometimes, or not twice in a row; different reactions for hits from the front and from behind; parry some swings and dodge others; parry only light attacks; not react to what the enemy cannot see; a counter after a parry; reactions only in one phase.
How a reaction is picked
Something happens to the enemy (it takes damage, a swing is announced near it) and arrives as a stimulus tag. Each trigger rule (unreal.SECReactionTriggerRule) in TriggerRules that answers that tag, asked highest RulePriority first, rolls its Chance; the first that fires picks one of the reactions it links, weighted by WeightMultiplier. A set with no trigger rules reacts to nothing.
StimulusArrives when
SEC.Stimulus.DamageTakenThe enemy takes a hit (the duplicated demo Blueprint reports it).
SEC.Stimulus.AttackTelegraphedA swing near it is announced by a SEC Telegraph Window on the attacker's montage.
SEC.Stimulus.StanceBrokenThe enemy's Blueprint reports it when the stance vital empties (AgentGuide/VitalsEncountersDebug.md); the plugin never sends it on its own.
A rule's fields
FieldDoes
RuleIdName in logs.
OnStimulusThe stimulus tag it answers.
RequireContextTags, BlockContextTagsTags the stimulus must or must not carry: the attacker's damage tags and the telegraph's Tags arrive here, so a parry rule can skip unblockable swings.
ConditionsGates that must all pass (below).
ReactionLinks (unreal.SECReactionTriggerLink)ReactionId, WeightMultiplier.
ReactionCategoryAnswer with any reaction of this category instead of named links.
RulePriorityHigher is asked first.
Chance0 to 1.
MinIntervalSeconds before the rule may fire again.
A reaction's fields (unreal.ReactionSpec)
ReactionId, ExecutionMethod (as an action's; montage_method from AGENTS.md), Priority, SelectionWeight, Cooldown (unreal.ActionCooldown), CustomScoring (Scorers, Gates), AddTags, RequiresTags, BlockTags (tags on the enemy itself), ReactionCategory, bCancelCurrentAction (cut the attack it was making).
SEC.State.ReactionActive in one reaction's AddTags and another's BlockTags stops them overlapping.
Reaction gates
Put them in a rule's Conditions or a reaction's CustomScoring.Gates.
Class (picker label)Passes whenFields
USECGate_IncomingAngle (Incoming Angle Gate)The blow arrives inside an arc in frontHalfAngleDegrees, bPassWithoutDirection, bInvert (behind instead)
USECGate_TimeToImpact (Time To Impact Gate)Enough time is left before an announced swing landsMinSeconds, MaxSeconds
USECGate_StimulusMagnitude (Stimulus Magnitude Gate)The blow is worth answeringMinMagnitude, MaxMagnitude
USECGate_SourceDistance (Source Distance Gate)The attacker is in rangeMinDistance, MaxDistance (cm), bPassWithoutSource
The angle gate tells front from back only: a side hit plays the front or the back reaction.
Recipe: hits from the front and from behind
def angle_gate(owner, half, invert):
    g = unreal.new_object(unreal.SECGate_IncomingAngle, outer=owner)
    g.set_editor_property("HalfAngleDegrees", half)
    g.set_editor_property("bInvert", invert)
    return g
 
def reaction(owner, rid, montage, gates, cooldown=1.0):
    r = unreal.ReactionSpec()
    r.set_editor_property("ReactionId", rid)
    r.set_editor_property("ExecutionMethod", montage_method(owner, montage))
    scoring = r.get_editor_property("CustomScoring")
    scoring.set_editor_property("Gates", gates)
    r.set_editor_property("CustomScoring", scoring)
    cd = r.get_editor_property("Cooldown")
    cd.set_editor_property("Duration", cooldown)
    r.set_editor_property("Cooldown", cd)
    return r
 
front = reaction(rset, "HitFront", am_hit_front, [angle_gate(rset, 90.0, False)])
back = reaction(rset, "HitBack", am_hit_back, [angle_gate(rset, 90.0, True)])
rule = unreal.SECReactionTriggerRule()
rule.set_editor_property("RuleId", "FlinchOnHit")
rule.set_editor_property("OnStimulus", tag("SEC.Stimulus.DamageTaken"))
links = []
for rid in ("HitFront", "HitBack"):
    link = unreal.SECReactionTriggerLink()
    link.set_editor_property("ReactionId", rid)
    links.append(link)
rule.set_editor_property("ReactionLinks", links)
rule.set_editor_property("Chance", 0.7)
rule.set_editor_property("MinInterval", 1.5)
rset.set_editor_property("Reactions", [front, back])
rset.set_editor_property("TriggerRules", [rule])
save(rset)
Check: sec.validate_asset(rset) returns nothing; in Play, SEC.Debug.LogReactions 1 logs each rule that fires or is skipped, with the reason.
Parry some swings, dodge others
Two rules on SEC.Stimulus.AttackTelegraphed: the parry rule at RulePriority 10 with Chance 0.4, MinInterval 2 and a BlockContextTags of the tag the unblockable swings' telegraph carries; the dodge rule at priority 0 with Chance 0.5. A swing the parry rule skips or does not roll falls to the dodge. A Time To Impact Gate (MinSeconds 0.15) on each keeps the enemy from starting an answer it cannot finish. The attacker's montage needs a SEC Telegraph Window with a Broadcast.Shape ( AGENTS.md, Recipe 4).
For i-frames on the dodge, a SEC State Tag Window holding SEC.State.Intangible over its evasive frames. A parry that stops the hit is a defense rule: AgentGuide/WeaponsDamageDefense.md.
Noticing
What the enemy can notice before an attack lands: StimulusFilters on the AI Config (USECStimulusFilter_FacingCone HalfAngleDegrees, USECStimulusFilter_Awareness MinimumState, USECStimulusFilter_LineOfSight TraceChannel, EyeHeight; each has BypassOnContextTags). Without them it notices everything in range.
Movesets per weapon and per role
WeaponReactionSet on a weapon's class defaults wins while it is equipped; RoleReactionSets on the AI Config takes unreal.RoleReactionSetConfig rows per role.
Mistakes
SymptomCause and fix
Never reactsNo trigger rule, or the rule's ReactionLinks name an ID no reaction has.
Never parriesThe attacker's montage has no telegraph window or its shape is empty, or Chance rolls low; a valid low chance can miss several swings in a row.
Reacts to every hit in a comboRaise the rule's MinInterval or the reaction's Cooldown.Duration.
Back hits play the front reactionBoth reactions lack an angle gate, or both have bInvert the same way.

Vitals, death, encounters, settings and debugging (AgentGuide/VitalsEncountersDebug.md)

Tasks: more health; a stance meter that breaks into a stun; stamina for the enemy; what happens on death; a fight that starts when the player walks in and shows a boss bar; turning off the demo hit effects; finding out why an enemy does nothing.
Vitals
The pawn's SEC Vitals component (Vitals) holds Vitals rows (unreal.SECVitalDefinition): VitalTag, MaxValue, RegenRatePerSecond, RegenDelaySeconds, DepletedLooseTag (held while empty), DepletedResultTag (on the hit that empties it), DepletedGameplayEvent (a gameplay event sent to the enemy's own ability system once when it empties, starting a granted ability that lists the tag as a trigger; a Reaction Set never hears it). The duplicated demo enemy has health and stance rows.
A stance meter that breaks:
vit = defaults(bp).get_editor_property("Vitals")
rows = list(vit.get_editor_property("Vitals"))
names = [str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("VitalTag"))) for r in rows]
if "SEC.Vital.Stance" in names:                   # the duplicated demo enemy has one
    stance = rows[names.index("SEC.Vital.Stance")]
else:
    stance = unreal.SECVitalDefinition()
    stance.set_editor_property("VitalTag", tag("SEC.Vital.Stance"))
    rows.append(stance)
stance.set_editor_property("MaxValue", 100.0)
stance.set_editor_property("RegenRatePerSecond", 15.0)
stance.set_editor_property("RegenDelaySeconds", 2.0)
stance.set_editor_property("DepletedLooseTag", tag("SEC.State.StanceBroken"))
vit.set_editor_property("Vitals", rows)
save(bp)
Attacks drain it through AdditionalVitalDamage on their damage configs (AgentGuide/WeaponsDamageDefense.md). DepletedLooseTag holds SEC.State.StanceBroken on the enemy from the moment the pool empties until it starts refilling, 2 seconds here; list that tag in each action's BlockTags so a stunned enemy does not swing.
The stun animation is a reaction on SEC.Stimulus.StanceBroken (AgentGuide/Reactions.md) playing the customer's stun montage, and the plugin never sends that stimulus. Sending it is a Blueprint step for the customer: in the enemy Blueprint, bind the Vitals component's On Vital Depleted, and when its Vital Tag is SEC.Vital.Stance, call Report Stimulus To with Receiver set to the enemy and a stimulus whose Tags hold SEC.Stimulus.StanceBroken. The call reports on the server only and does nothing on a client.
Which vital counts as health and as stamina is HealthVitalTag and StaminaVitalTag in Project Settings.
Death
With bAutoHandleDeathOnHealthDepleted on (the default) on the controller's CombatControllerComponent, an emptied health vital shuts the enemy down: brain, actions, reactions, traces, role. bDropWeaponOnDeath drops its weapon. Playing a death montage or a ragdoll is the controller's On Death event in Blueprint, a step for the customer; the demo Blueprints show it.
Encounters and boss bars
A placed SEC Encounter Trigger (ASECEncounterTrigger) names a place or opens a fight when the player walks into its TriggerBox: EncounterName, Enemies (on the placed actor only), Payload, bTriggerOnce, StartCondition (PLAYER_ENTERS, MANUAL), bEndWhenPlayerLeaves. The demo's /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger also ends the fight as Defeated once its enemies die, so spawn that one (AGENTS.md, Recipe 5, step 4).
A boss bar is a widget bound to the SEC Encounter Subsystem's On Encounter Started event; the demo's WBP_SEC_HUD_Encounter is one. Adding it to the customer's HUD is a Blueprint step.
Project Settings
unreal.get_default_object(unreal.SoulslikeEnemyCombatSettings): RoleLimits, AdditionalRoles, RoleReassignmentInterval, MinTimeInRole, TokenBudgets, DefaultTargetSelectorClass, DefaultTargetFilterClass, HealthVitalTag, StaminaVitalTag. The demo hit effects are switched off with Load Demo Hit Effects under Project Settings, Plugins. Before UE 5.8 a value set from Python lasts until the editor closes (AgentGuide/Python.md), so ask the customer to set it in Project Settings, Plugins, Soulslike Enemy Combat.
Debugging an enemy
Console command (in Play)Shows
SEC.Debug.All 1Every overlay above each enemy: awareness, brain, action, role, team
SEC.Debug.WatchPawn <name>Narrows the overlays to one enemy
SEC.Debug.Scoring 1Each action's score, and why a gate refused it
SEC.Debug.LogActionDecisions 1, SEC.Debug.LogActionExecution 1Each pick, and each action starting and ending
SEC.Debug.LogReactions 1Each trigger rule firing or skipped, with the reason
SEC.Debug.LogRoleAssignments 1Role assignments
SEC.Debug.Melee.DrawTracing 1Every melee sweep
SEC.Debug.Movement.DrawScoring 1, SEC.Debug.Movement.DrawAvoidance 1Movement direction scores and avoidance
SEC.Debug.Threat.Draw 1The player's look cone and the threat level
SEC.Debug.Stimulus.Draw 1Announced swings and who they reach
SEC.Debug.Warp 1Where each motion warp lands
SEC.Logs VerboseRaises every SEC log category; SEC.Logs puts them back
After Play, log_warnings() returns SEC's warnings, each naming the asset and the fix. sec.validate_asset(asset) checks an Action Set, Reaction Set or damage config without playing.
Mistakes
SymptomCause and fix
Never diesNo SEC.Vital.Health row, or bAutoHandleDeathOnHealthDepleted off.
The stance never breaksNo stance row on the enemy, or the attacks' damage configs drain none.
The stance empties but no stun playsNothing reports SEC.Stimulus.StanceBroken (the On Vital Depleted step above), or the Reaction Set has no rule on it.
The fight never startsThe trigger box does not cover where the player enters, StartCondition is MANUAL, or it already fired with bTriggerOnce.
A Project Settings value is gone after a restartIt was set from Python; set it in Project Settings.

Weapons, damage and defense (AgentGuide/WeaponsDamageDefense.md)

AGENTS.md, Recipe 2, makes a weapon and Recipe 1, Step 3, a damage config. This file covers the rest: loadouts, hit volumes, what a hit is worth beyond health, effects on landing, and an enemy that blocks or parries.
Tasks: drop the weapon on death; several weapons; heavy hits that drain stamina or stance and knock the player back; sparks on metal; swings that spare other enemies; an enemy that blocks frontal hits while guarding; a parry that turns a blow aside.
Weapons and loadout
A weapon is a Blueprint of ASECWeaponBase (duplicate /SoulslikeEnemyCombat/Demo/Weapons/BP_SEC_WeaponBase). Class defaults: TraceSockets (unreal.SECTraceSocket: ID, SourceComponentTag, Shape (unreal.SECTraceShape: SPHERE, CAPSULE, CAPSULE_TWO_POINT), SocketOrBoneName, EndSocketOrBoneName, Radius, CapsuleHalfHeight, LocalOffset), AttachSocket, AttachOffset, AnimLayerToUse, WeaponActionSet, WeaponReactionSet, WeaponDefenseSet.
The pawn's SEC Equipment Component (EquipmentComponent): StartingLoadout (unreal.SECEquipLoadoutEntry: EquippableClass, SocketOverride, bUseTransformOverride, TransformOverride, bUseAsWeapon), bEquipStartingLoadoutOnBeginPlay, SwapDisposal, TeardownDisposal, DropLifetime. The controller's SEC Combat Controller (CombatControllerComponent) has bDropWeaponOnDeath.
Damage configs
USECDamageConfig: Damage, AdditionalVitalDamage (unreal.SECVitalDamage rows VitalTag, Amount), DamageTypeClass, DamageTags, MultiHitInterval (seconds before the same swing hits one target again; -1 once), MaxTargetsPerSwingOverride (-1 uses the trace's), bUseAuthoredHitDirection, AuthoredHitDirectionLocal, HitEffects.
Hit effect (picker label)DoesFields
USECGameplayCueHitEffect (SEC Play Gameplay Cues)Plays cues on the attacker and the targetAttackerCues, TargetCues, SurfaceSet (USECImpactSurfaceSet: cue suffix per physical material or surface)
USECPhysicsImpulseHitEffect (SEC Apply Physics Impulse)Shoves the struck bodyHitBoneForce, HitOverallForce, HitRotationalForce, bVelocityChange
USECApplyGameplayEffectHitEffect (SEC Apply Gameplay Effect)Applies a gameplay effectGameplayEffect, Level
Every hit effect also has AffectedSides, SuppressOnResultTags, RequireAnyResultTags (run only on Blocked, Parried and the like), RequireAnyTargetTags, SuppressOnTargetTags, bRequireDamageApplied, bRunOnSceneryHits. Make each with the damage config as outer.
A heavy hit that drains stance and knocks back:
dmg = new_data_asset(unreal.SECDamageConfig, F, f"Damage_{N}_Slam")
dmg.set_editor_property("Damage", 35.0)
stance = unreal.SECVitalDamage()
stance.set_editor_property("VitalTag", tag("SEC.Vital.Stance"))
stance.set_editor_property("Amount", 40.0)
dmg.set_editor_property("AdditionalVitalDamage", [stance])
push = unreal.new_object(unreal.SECPhysicsImpulseHitEffect, outer=dmg)
push.set_editor_property("HitOverallForce", 800.0)
dmg.set_editor_property("HitEffects", [push])
save(dmg)
The vital must exist on the target: a row on its vitals component (AgentGuide/VitalsEncountersDebug.md).
The pawn's melee trace
MeleeTraceComponent on the pawn: DefaultDamageConfig, TeamFilter (unreal.SECTeamFilter: NO_FILTER, ENEMIES_ONLY, FRIENDLIES_ONLY, NEUTRAL_ONLY), MaxTargetsPerSwing, HitResetInterval, PassThroughTags (SEC.State.Intangible lets i-frames work), SubStepDistance, bDrawDebug. The demo enemy's TeamFilter is ENEMIES_ONLY, so its swings spare other enemies.
Blocks and parries on the enemy
A Defense Rule Set (USECDefenseRuleSet) lists Responses run in order and OutcomeEffects. The pawn's SEC Defense component (Defense) runs its DefenseSet and Responses; a weapon lends its WeaponDefenseSet while equipped.
Response (picker label)DoesFields
USECDefenseRule_Guard (SEC Guard)Soaks a hit arriving inside an arcArcHalfAngleDegrees, DamageMultiplier (0 blocks all)
USECDefenseRule_Scale (SEC Scale Damage)Scales a hitDamageMultiplier
USECDefenseRule_Invulnerable (SEC Invulnerable)Takes nothing
Every response has RequiredTags and BlockedTags (shown as Required and Blocked Defender Tags: what the enemy must hold, such as SEC.State.Blocking), ResultTag (what the hit is called: the attacker's effects read it), RequiredAttackTags, BlockedAttackTags, and a cost (CostVitalTag, FlatCost, CostPerDamage).
Blocks from the front while guarding:
rules = new_data_asset(unreal.SECDefenseRuleSet, F, f"DefenseRuleSet_{N}")
guard = unreal.new_object(unreal.SECDefenseRule_Guard, outer=rules)
guard.set_editor_property("RequiredTags", tags("SEC.State.Blocking"))
guard.set_editor_property("ArcHalfAngleDegrees", 70.0)
guard.set_editor_property("DamageMultiplier", 0.0)
rules.set_editor_property("Responses", [guard])
save(rules)
defaults(bp).get_editor_property("Defense").set_editor_property("DefenseSet", rules)
save(bp)
The guard holds while the enemy carries SEC.State.Blocking: a SEC State Tag Window on a block reaction's montage, or AddTags on a block action or reaction. The demo's two-handed weapon uses DefenseRuleSet_SEC_TwoHanded and GA_SEC_TwoHanded_Block. For a parry, a second Guard response with a narrower arc, RequiredTags of the parry window's tag and its own ResultTag, placed first.
Check: reload the rule set and print each response's class, tags and arc. In Play, a frontal hit while the enemy blocks deals nothing.
Mistakes
SymptomCause and fix
Hits pass throughSocketIDs on the window differ from the weapon's TraceSockets ID, or bUseAsWeapon is off on the loadout entry.
Enemies hit each otherThe trace's TeamFilter is NO_FILTER; set ENEMIES_ONLY.
A stance or stamina drain does nothingThe target has no row for that vital.
The enemy never blocksNothing gives it SEC.State.Blocking while it guards, or the rule set is on neither the pawn's Defense component nor its weapon.