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

> 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++.

Markdown version of https://rigbak.com/docs/soulslike-combat/agents, a page of the Soulslike Enemy Combat documentation. Link that page when citing it.

> 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. **Open your project**

   Open the project in the Unreal Editor and leave it open. The agent works in the running editor.
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. **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. **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.

| Route | Engine versions | What it needs |
| --- | --- | --- |
| **Python bridge** | 5.6, 5.7 and 5.8 | The 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 MCP** | 5.8 | The 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

| Ask | What 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

| File | What it is |
| --- | --- |
| `Resources/AGENTS.md` | The guide, in full below. |
| `Resources/AgentBridge/run_in_editor.py` | The Python bridge: sends the agent's Python to the open editor and prints what the editor printed. |
| `Resources/AgentSkills/sec-agent/SKILL.md` | The `/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](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`:

     ```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:

  ```ini
  [/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:

```python
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.

```python
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`:

| Call | Does |
| --- | --- |
| `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 step | Epic Unreal MCP tool |
| --- | --- |
| `load`, find assets | `AssetTools.find_assets`, `AssetTools.load_asset` |
| `duplicate` | `AssetTools.duplicate` |
| `new_data_asset` | `DataAssetTools.create` |
| Blueprint with a parent class | `BlueprintTools.create` |
| `defaults(bp)` and setting class defaults | `BlueprintTools.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 Blueprint | `ActorTools.add_component` |
| `save` | `AssetTools.save_assets` |
| Play and look | `EditorAppToolset.StartPIE`, `StopPIE`, `CaptureViewport` |
| `log_warnings` | `LogsToolset.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:

```python
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.

```python
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.

```python
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:

```python
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.

```python
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.

```python
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.

```python
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.**

```python
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`.

```python
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

```python
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

```python
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:

```python
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:

```python
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.

```python
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:

```python
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

```python
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

```python
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:

| Want | Set |
| --- | --- |
| An attack for a range | A Distance Scorer with its own band. Give each attack a different band. |
| Only when facing the target | Add an **Angle Scorer** (`USECAngleScorer`); its default Range peaks at 0 to 30 degrees. |
| A three-hit combo | `ChainLinks` 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 another | `Selection` = `unreal.SECActionSelection.CHAIN_ONLY` |
| Fewer repeats | `Cooldown.Duration`, and `Cooldown.MaxConsecutiveUses` (-1 is unlimited) |
| Not as the opening move | `Cooldown.InitialCooldown` |
| A gap between any two attacks | `ActionRecoveryTime` on the AI Config (0 by default) |
| Several enemies take turns | Add a **Combat Token Gate** (`USECTokenGate`, `TokenTag` = `SEC.Token.Attack`) to `CustomScoring.Gates` |
| Back off when crowded | An 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 gap | A **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.

```python
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:

   ```python
   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:

```python
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.

| Task | File |
| --- | --- |
| A property refuses a value, rows in arrays, components on a Blueprint, montage sections, new gameplay tags, checking an asset, project settings | `AgentGuide/Python.md` |
| A moveset from several animations, ranges, combos and pacing, gates, cooldowns, one clip with several attacks, movesets per weapon or role | `AgentGuide/Actions.md` |
| Flinches by direction, parry and dodge rules, what an enemy can notice | `AgentGuide/Reactions.md` |
| Keeping distance, circling, backing off when stared at, flanking angles | `AgentGuide/Movement.md` |
| Target selection, combat roles and limits, taking turns, teams, the brain, repeatable fights | `AgentGuide/BrainRolesTargets.md` |
| Sight and hearing, patrols, guard posts, leashes | `AgentGuide/AwarenessTerritory.md` |
| An attack run as a behavior tree, the State Tree tasks | `AgentGuide/BehaviorTrees.md` |
| Montage windows, root-motion lunges, step-backs, tag-driven animation, walking slower on a tag | `AgentGuide/Animation.md` |
| Weapons and loadouts, damage and hit effects, enemies that block and parry | `AgentGuide/WeaponsDamageDefense.md` |
| Health, stamina and stance, death, encounters and boss bars, settings, debugging | `AgentGuide/VitalsEncountersDebug.md` |

### Reference

#### Assets you create

| Asset | Class | Made with | Holds |
| --- | --- | --- | --- |
| Enemy pawn | Blueprint of `AEnemyCharacterBase` (duplicate of `BP_SEC_EnemyCharacter`) | `duplicate` | Mesh, `AIConfig`, `StartingAbilities`, components below |
| AI controller | `AEnemyControllerBase`; the demo `BP_SEC_EnemyController` is fine | already set on the duplicate | Movement, scoring, reactions, awareness, the brain |
| Enemy AI Config | `UEnemyAIConfig` | `new_data_asset` | `TargetSelector`, `AwarenessConfig`, `DefaultActionSet`, `DefaultReactionSet`, `DefaultMovementProfile`, `StateTree`, `RoleRegistrationParams`, `ActionRecoveryTime` |
| Action Set | `UActionSet` | `new_data_asset` | `Actions` (`FActionSpec`) |
| Reaction Set | `UReactionSet` | `new_data_asset` | `Reactions` (`FReactionSpec`), `TriggerRules` (`FSECReactionTriggerRule`) |
| Awareness Config | `USECAwarenessConfig` | `new_data_asset` | Sight, hearing, `bAlwaysAware` |
| Movement Behavior Profile | `UMovementBehaviorProfile` | `new_data_asset` | `DesiredDistance` (cm) |
| Damage Config | `USECDamageConfig` | `new_data_asset` | `Damage`, `DamageTags`, `HitEffects` |
| Weapon | Blueprint of `ASECWeaponBase` (duplicate of `BP_SEC_WeaponBase`) | `duplicate` | `TraceSockets`, `AttachSocket`, `AttachOffset`, `WeaponActionSet`, `WeaponReactionSet` |
| Animation Blueprint | Child of `ABP_SEC_LocomotionTemplate` | `sec.create_locomotion_anim_blueprint` | `IdleAnimation`, `WalkAnimation`, `RunAnimation`, `WalkSpeed`, `RunSpeed` |
| Montage | `UAnimMontage` | `sec.create_montage_from_animation` | Notify 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

| Asset | Path |
| --- | --- |
| 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) | Class | Needed for | Key properties |
| --- | --- | --- | --- |
| SEC Melee Trace Window | `USECMeleeTraceNotifyState` | Any hit to deal damage | `SocketIDs`, `DamageConfig`, `ExtraDamageTags` |
| SEC Telegraph Window | `USECTelegraphNotifyState` | Enemies parrying or dodging this attack | `Broadcast.Shape`, `bFindAttackAutomatically`, `Tags` |
| SEC State Tag Window | `USECStateTagNotifyState` | Tags for a stretch: `SEC.State.Intangible` for i-frames | `Tags` |
| SEC Approach Window | `USECApproachWindowNotifyState` | Turning and gliding toward the target during a wind-up | `Mode`, `ApproachTargetName` (`Target`), `StopDistanceOverride` (cm between the bodies) |
| SEC Gameplay Event | `USECGameplayEventNotify` | Sending a gameplay event at a frame | `EventTag` |
| SEC Anim Effect Window, SEC Play Anim Effect | `USECAnimEffectWindowNotifyState`, `USECAnimEffectNotify` | Trails, footsteps, foley | `Effect` |

SEC has no combo notify: combos come from `ChainLinks` on the Action Set.

#### Gameplay tags you set

| Tag | Use |
| --- | --- |
| `SEC.Damage.Physical.Slashing`, `.Piercing`, `.Bludgeoning`; `SEC.Damage.Elemental.Fire`, `.Ice`, `.Lightning` | `DamageTags` on a damage config |
| `SEC.Stimulus.DamageTaken` | Trigger rule for a flinch |
| `SEC.Stimulus.AttackTelegraphed` | Trigger rule for a parry or dodge |
| `SEC.Role.Attacker`, `.Flanker`, `.Waiter`, `.Supporter`, `.Elite` | Role settings and per-role sets |
| `SEC.Token.Attack` | Combat Token Gate |
| `SEC.State.Intangible` | i-frames in a State Tag Window |
| `SEC.State.IsMovementBlocked` | Already in each new action's `AddTags` |
| `SEC.State.ReactionActive` | Stops two reactions overlapping (`AddTags` on one, `BlockTags` on the other) |
| `SEC.Action.BlockActions` | On the ability system, stops all actions |
| `SEC.Vital.Health`, `SEC.Vital.Stamina`, `SEC.Vital.Stance` | Vitals rows |

#### Common mistakes

| Symptom | Cause and fix |
| --- | --- |
| Enemy stands still, no overlay above it | The pawn's AI controller is not an SEC one, or `AIConfig` is empty. |
| Enemy never notices the player | `AwarenessConfig` empty, or the player is on the enemy's team (Team Id 1). |
| Enemy moves but never attacks | No 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 move | The action has no Distance Scorer. |
| Action wins and nothing plays | `ExecutionMethod` empty, payload `Montage` empty, or `ActionId` empty. |
| Attacks once, then freezes | The ability never reaches End Ability. Use `GA_SEC_MontageAbility`, or set `AbilityTimeout` on the execution method. |
| Hits pass through | `SocketIDs` 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 other | The 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 parries | The attacker's montage has no SEC Telegraph Window, its `Broadcast.Shape` is empty, or the Reaction Set has no trigger rule. |
| Never flinches | The pawn does not report damage (see Recipe 1, Step 7), or the set has no rule on `SEC.Stimulus.DamageTaken`. |
| Does not move at all | No navmesh, or a leftover `SEC.State.IsMovementBlocked` tag. |
| T-pose or no animation | The 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](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.

| Topic | Page |
| --- | --- |
| Overview | [https://rigbak.com/docs/SoulslikeCombatDocs](https://rigbak.com/docs/SoulslikeCombatDocs) |
| First enemy | [https://rigbak.com/docs/soulslike-combat/getting-started](https://rigbak.com/docs/soulslike-combat/getting-started) |
| Full enemy walkthrough | [https://rigbak.com/docs/soulslike-combat/step-by-step-enemy](https://rigbak.com/docs/soulslike-combat/step-by-step-enemy) |
| Smallest possible enemy | [https://rigbak.com/docs/soulslike-combat/minimal-enemy](https://rigbak.com/docs/soulslike-combat/minimal-enemy) |
| Actions, scoring, combos | [https://rigbak.com/docs/soulslike-combat/action-system](https://rigbak.com/docs/soulslike-combat/action-system) |
| Action Set editor | [https://rigbak.com/docs/soulslike-combat/action-set-editor](https://rigbak.com/docs/soulslike-combat/action-set-editor) |
| Reactions and trigger rules | [https://rigbak.com/docs/soulslike-combat/reaction-system](https://rigbak.com/docs/soulslike-combat/reaction-system) |
| Reaction Set editor | [https://rigbak.com/docs/soulslike-combat/reaction-set-editor](https://rigbak.com/docs/soulslike-combat/reaction-set-editor) |
| Montages, notifies, root motion | [https://rigbak.com/docs/soulslike-combat/animation-integration](https://rigbak.com/docs/soulslike-combat/animation-integration) |
| Weapons | [https://rigbak.com/docs/soulslike-combat/weapons](https://rigbak.com/docs/soulslike-combat/weapons) |
| Hit detection | [https://rigbak.com/docs/soulslike-combat/melee-trace](https://rigbak.com/docs/soulslike-combat/melee-trace) |
| Blocks, parries, damage answers | [https://rigbak.com/docs/soulslike-combat/defense-system](https://rigbak.com/docs/soulslike-combat/defense-system) |
| Health and stamina | [https://rigbak.com/docs/soulslike-combat/vitals](https://rigbak.com/docs/soulslike-combat/vitals) |
| Telegraphs | [https://rigbak.com/docs/soulslike-combat/attack-telegraphs](https://rigbak.com/docs/soulslike-combat/attack-telegraphs) |
| Sight, hearing, stealth | [https://rigbak.com/docs/soulslike-combat/awareness-system](https://rigbak.com/docs/soulslike-combat/awareness-system) |
| Movement and spacing | [https://rigbak.com/docs/soulslike-combat/movement-system](https://rigbak.com/docs/soulslike-combat/movement-system) |
| Targets and teams | [https://rigbak.com/docs/soulslike-combat/targeting-system](https://rigbak.com/docs/soulslike-combat/targeting-system) |
| Combat roles | [https://rigbak.com/docs/soulslike-combat/combat-roles](https://rigbak.com/docs/soulslike-combat/combat-roles) |
| Taking turns | [https://rigbak.com/docs/soulslike-combat/combat-tokens](https://rigbak.com/docs/soulslike-combat/combat-tokens) |
| Patrol posts and leashes | [https://rigbak.com/docs/soulslike-combat/territory](https://rigbak.com/docs/soulslike-combat/territory) |
| StateTree tasks | [https://rigbak.com/docs/soulslike-combat/statetree](https://rigbak.com/docs/soulslike-combat/statetree) |
| Every setting | [https://rigbak.com/docs/soulslike-combat/configuration](https://rigbak.com/docs/soulslike-combat/configuration) |
| Recipes (bosses, packs) | [https://rigbak.com/docs/soulslike-combat/patterns](https://rigbak.com/docs/soulslike-combat/patterns) |
| Multiplayer | [https://rigbak.com/docs/soulslike-combat/multiplayer](https://rigbak.com/docs/soulslike-combat/multiplayer) |
| Troubleshooting | [https://rigbak.com/docs/soulslike-combat/troubleshooting](https://rigbak.com/docs/soulslike-combat/troubleshooting) |
| Blueprint and C++ API | [https://rigbak.com/docs/soulslike-combat/api-reference](https://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.

```python
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`)

| Field | Does |
| --- | --- |
| `ActionId` | Unique name in the set; chain links name it. An empty one is dropped. |
| `bEnabled` | Off 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) |
| `ExecutionMethod` | How it runs (below). |
| `SelectionWeight`, `RiskPenalty` | Multiply and divide the score: below 1 and above 1 make it rarer. |
| `CustomScoring` (`unreal.SECCustomScoring`) | `Scorers` (soft factors) and `Gates` (hard vetoes), below. |
| `TagScoreMultipliers` | Tag to multiplier: while the target holds the tag, the score is multiplied. |
| `RequiresTags`, `BlockTags` | Tags that must be, or must not be, on the target or in the world. |
| `bRequireLineOfSight` | Only with a clear line to the target. |
| `AddTags` | Tags 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`, `RecoveryTime` | This 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`) |
| `Hook` | A `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) | Runs | Fields |
| --- | --- | --- |
| `USECAbilityExecutionMethod` (Gameplay Ability) | One ability and waits for it to end | `AbilityClass` (`GA_SEC_MontageAbility` plays the payload's montage), `AbilityTimeout`, `Payload` |
| `USECAbilityByTagExecutionMethod` (Gameplay Ability By Tag) | The granted ability carrying a tag | `ActivationTag`, `AbilityTimeout`, `Payload` |
| `USECAbilityFireAndForgetExecutionMethod` (Gameplay Ability (Fire And Forget)) | Starts an ability and ends the action at once | `AbilityClass`, `ActivationTag` |
| `USECBehaviorTreeExecutionMethod` (Behavior Tree Sequence) | Behavior trees one after another | `AgentGuide/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`:

```python
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) | In | Fields |
| --- | --- | --- |
| `USECDistanceScorer` (Distance Scorer) | `Scorers` | `Range` (cm) |
| `USECAngleScorer` (Angle Scorer) | `Scorers` | `Range` (degrees off facing the target; default peaks at 0 to 30) |
| `USECHealthScorer` (Health Scorer) | `Scorers` | `Range` (own health, 0 to 1) |
| `USECSpeedScorer` (Speed Scorer) | `Scorers` | `Range` (own speed, cm/s) |
| `USECVitalScorer` (Vital Scorer) | `Scorers` | `VitalTag`, `bUseFraction`, `Range` |
| `USECAttributeScorer` (Attribute Scorer) | `Scorers` | `Attribute`, `NormalizeBy`, `ValueEval` |
| `USECStaminaGate` (Stamina Gate) | `Gates` | `MinStamina` |
| `USECVitalGate` (Vital Gate) | `Gates` | `VitalTag`, `MinValue`, `bUseFraction` |
| `USECTokenGate` (Combat Token Gate) | `Gates` | `TokenTag` (`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

| Symptom | Cause and fix |
| --- | --- |
| One attack fires at every range | It has no Distance Scorer. |
| The follow-up never plays | Its `ActionId` differs from the link's `TargetActionId`, or its own band leaves it at 0 where the first attack ends. |
| The enemy spams one move | No `Cooldown.Duration`, or every other action scores 0 at that range. `SEC.Debug.Scoring 1` shows why. |
| An action wins and nothing plays | Empty `ExecutionMethod`, payload `Montage` or `ActionId`. A whole-array write does not fill the method; make it yourself. |
| The back-step plays at range | Its 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) | Class | Fields |
| --- | --- | --- |
| SEC Melee Trace Window | `USECMeleeTraceNotifyState` | `SocketIDs` (weapon trace socket IDs; empty sweeps all), `DamageConfig`, `ExtraDamageTags` |
| SEC Approach Window | `USECApproachWindowNotifyState` | `Mode` (`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 Window | `USECStateTagNotifyState` | `Tags` held while it runs: `SEC.State.Intangible` (i-frames), `SEC.State.Blocking` |
| SEC Telegraph Window | `USECTelegraphNotifyState` | `Broadcast` (`unreal.SECTelegraphBroadcast`: `Shape` = `USECTelegraphShape_Sphere` `Radius` or `USECTelegraphShape_Cone` `Radius`, `HalfAngleDegrees`), `bFindAttackAutomatically`, `DamageConfig` (shown as Attack), `Tags`, `Magnitude` |
| SEC Anim Effect Window | `USECAnimEffectWindowNotifyState` | `Effect` (`EffectTags`, `SocketName`), answered by an anim effect handler on the pawn |
| SEC Gameplay Event (one frame) | `USECGameplayEventNotify` | `EventTag` |
| SEC Play Anim Effect (one frame) | `USECAnimEffectNotify` | `Effect` |

##### 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:

```python
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

| Symptom | Cause and fix |
| --- | --- |
| Swings short of the target | No approach window, or `StopDistanceOverride` left at -1 (the approach component's 150). |
| The lunge overshoots | The Motion Warping window has no modifier, or its `WarpTargetName` is not `Target`. |
| The dodge does not avoid hits | No `SEC.State.Intangible` window over the evasive frames. |
| Nothing happens on an effect window | The 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`.

| Field | Does |
| --- | --- |
| `SightRadius`, `LoseSightRadius` | Sight range in cm, and the range at which a seen target is lost |
| `PeripheralVisionHalfAngle` | Half the sight cone, in degrees (90 is a 180 degree cone) |
| `bEnableHearing`, `HearingRange` | Hearing and its range in cm |
| `bEnableDamageSense` | Being hit reveals the attacker |
| `bGradualEscalation`, `EscalationRate`, `SuspicionThreshold` | Sight fills a 0 to 1 meter, faster up close and straight ahead, before the enemy fully notices; the threshold is where it turns suspicious |
| `ProximityRadius` | Inside this many cm a target stays noticed without sight |
| `GraceLossDuration` | Seconds it keeps fighting after losing sight |
| `SuspiciousDecayDuration` | Seconds a full meter takes to drain |
| `ConfirmLostDelay` | Seconds without sight before a noticed target counts as lost |
| `bAlwaysAware` | Knows every hostile target at once, no sight needed (a boss in its arena) |
| `bAlertAlliesOnDetect`, `AllyAlertRadius`, `AllyAlertCooldown` | Tells nearby allies when it notices someone |

**A slow, side-blind guard:**

```python
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`).

```python
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 field | Does |
| --- | --- |
| `Posts` | The posts this enemy owns, in route order |
| `RestingBehavior` | `PATROL_POSTS` (walk the route) or `HOLD_GROUND` (stand at the first post) |
| `PostOrder` | `SEQUENTIAL_LOOP`, `SEQUENTIAL_PING_PONG`, `RANDOM` |
| `PatrolStartDelay`, `PatrolStartVariance`, `ArrivalRadius` | When it sets off, and how close counts as arrived |
| `bLeashEnabled`, `LeashAnchor` (`SELF`, `POSTS_CENTERED`, `POSTS_UNION`, `ACTOR` with `LeashAnchorActor`), `LeashRadius` | How far it may be dragged from its ground |
| `LeashBreakGraceTime` | Seconds past the leash before it gives up |
| `bOnlyFightInsideLeash` | Ignore targets outside the leash |
| `DisengageBehavior` | `RETURN_TO_POST` or `SETTLE_HERE` once a fight ends |

| Post field | Does |
| --- | --- |
| `Shape` | `POINT`, `RADIUS` (wander inside `WanderRadius`), `BOX` (inside `WanderBoxExtent`) |
| `WaitTime`, `WaitTimeVariance` | Seconds it waits on arrival |
| `bFaceForwardOnArrival` | Turn to the post's facing |
| `bPassThrough` | Walk 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

| Symptom | Cause and fix |
| --- | --- |
| Never notices the player | `AwarenessConfig` empty, the player beyond `SightRadius`, outside the cone, or on the enemy's team. |
| Notices through walls | `ProximityRadius` large, or hearing on with a loud level. |
| Stands still instead of patrolling | `Posts` empty, `RestingBehavior` on `HOLD_GROUND`, no navmesh between the posts, or a `StateTree` without the territory tasks. |
| Chases forever | `bLeashEnabled` off, or the leash anchor is `SELF` with `bTrackLeashAnchor` on. |
| The patrol is lost after a restart | The 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.

| Field | Does |
| --- | --- |
| `AbilityToGrant` | The gameplay ability the trees start (`GA_SEC_MontageAbility` plays the payload's montage) |
| `BehaviorTreeSequence` | The trees, in order |
| `BehaviorTreeTimeout` | Seconds before a tree is stopped |
| `PayloadKeyName` | Blackboard key the payload is written to, `SEC_Payload` |
| `Payload` | A `USECMontagePayload` (outer: the method), as for an ability method |

The plugin's tasks, which the editor's behavior tree palette lists:

| Task (class) | Does | Fields |
| --- | --- | --- |
| Move Until Distance (`UBTTask_MoveUntilDistance`) | Walks toward the blackboard target until within reach | `TargetDistance` (cm), `MaxChaseDistance`, `TimeoutSeconds`, `bAllowStrafe`, `bUsePathfinding`, `bAllowPartialPath` |
| Activate Action Ability (`UBTTask_ActivateActionAbility`) | Starts the ability the running action named and waits for it to end | `FallbackActivationTag`, `bFailWhenNoAbility` |
| Activate Ability (`UBTTask_ActivateAbility`) | Starts a named ability | `AbilityClass`, `ActivationMode` (`unreal.AbilityActivationMode.BY_TAG`, `BY_EVENT`), `EventTag` (shown as Activation Tag), `bGrantAbilityIfMissing` |
| Make Distance (`UBTTask_MakeDistance`) | Backs away from the target without pathfinding | `TargetDistance` (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:

```python
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:

| Task | Does |
| --- | --- |
| Build Decision Context | Gathers distance, angle, health and threat each tick |
| Poll Action, Do Action | Scores the actions and runs the chosen one |
| AI Movement | Runs the movement profile |
| Poll Combat Role, Poll Focus Actor, Poll Last Known Location | Read the role, the focus target, where the target was last seen |
| Sync Action Set, Sync Reaction Set, Sync Movement Profile, UpdateActionSet | Swap sets on a role change, or to a named Action Set |
| Territory Patrol, Territory Investigate, Return To Territory, Hold Out Of Combat, Monitor Combat Leash | Patrol, search, walk home, stand down, watch the leash |

| Condition | Passes when |
| --- | --- |
| Awareness State At Least | The 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 Is | Territory and leash state |
| Action Executing, Action Recovering, Actions Blocked, Reaction Executing, ASC Has Tag | Combat state |

##### Mistakes

| Symptom | Cause and fix |
| --- | --- |
| The action never ends | A tree waits forever: set `BehaviorTreeTimeout`, and check the ability reaches End Ability. |
| Activate Action Ability fails | `AbilityToGrant` is empty, so the enemy holds no ability to start. |
| Move Until Distance never arrives | No 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

| Field | Does |
| --- | --- |
| `TargetSelector` | How it picks a target (below). Empty picks the first registered target it may fight. |
| `TargetFilter` | Which 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). |
| `bIgnoreTargetRedistribution` | Keep its target when targets are shared out again. |
| `DefaultActionSet`, `RoleActionSets`; `DefaultReactionSet`, `RoleReactionSets`; `DefaultMovementProfile`, `RoleMovementProfiles` | What it fights, reacts and moves with, per role. |
| `AwarenessConfig`, `StimulusFilters` | What 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/`. |
| `DecisionContextParams` | `AggressionLevel` (0 to 2) and line-of-sight trace settings. |
| `ActionRecoveryTime`, `InterruptRecoveryTime`, `ActionRecoveryTimeRandomization` | Pause after each action (`AgentGuide/Actions.md`). |
| `bDeterministic`, `DefaultSeed`, `StartBarrierSeconds` | The 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:**

```python
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).

```python
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

| Symptom | Cause and fix |
| --- | --- |
| Every enemy goes for the same player | No selector, or a First or Closest selector; use Balanced or Random inside the awareness filter. |
| A role is never assigned | `bAutoRegisterForCombatRoles` off, the role is outside `AllowedRoles`, or its limit is full. |
| Enemies fight each other | They share no team and are on different `TeamId`s, or `bFreeForAll` is on. |
| Two enemies attack at once despite the token gate | The 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.

```python
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:

```python
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

| Field | Default | Does |
| --- | --- | --- |
| `DesiredDistance` | 400 | Distance in cm the enemy keeps from its target |
| `DistanceTolerance` | 0.2 | How far past `DesiredDistance` still counts as close enough, as a share of it (0.05 to 0.5) |
| `CrowdTolerance` | 0.45 | How far inside `DesiredDistance` the enemy tolerates before stepping back, as a share of it |
| `bEnableStrafing` | true | Circle the target once at the distance |
| `StrafePreferenceWeight` | 1 | How strongly it prefers circling over standing |
| `bAutoStrafeSwap`, `StrafeSwapThreshold`, `StrafeSwapCooldown` | true, 3, 2 | Switch circling direction when the other side scores that many times better |
| `bEnableStrafeTimeLimit`, `StrafeTimeLimit`, `StrafeTimeLimitFluctuation` | true, 5, 2.5 | Switch direction after circling one way this long (seconds, plus or minus the fluctuation) |
| `bRestAfterStrafeSwap`, `StrafeSwapRestTime`, `StrafeSwapRestFluctuation` | false, 0.5, 0.25 | Pause after a timed switch |
| `bEnableStrafeRest`, `StrafeRestTimeLimit`, `StrafeRestDuration` | true, 10, 1.5 | Stop and rest after circling this long |
| `bAdjustDistanceByThreat`, `ThreatDistanceScale` | false, 1 | Multiply the distance by 1 + threat x scale; threat climbs from 0 to 1 while the player looks at the enemy |
| `bSwapStrafeOnHighThreat` | false | Switch circling direction once the player has stared long enough |
| `PositioningRules` | empty | Extra 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

| Symptom | Cause and fix |
| --- | --- |
| The enemy hugs the player | `DesiredDistance` below about 150, or the profile is not the config's `DefaultMovementProfile` while `bManageMovementProfilesAutomatically` is on (the default). |
| It jitters back and forth | `DistanceTolerance` too small for the distance; raise it toward 0.2. |
| It stays close after each attack | An 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 attacks | It 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 at | `bAdjustDistanceByThreat` off, or the controller has no Threat Detection component (the demo controller has one). |
| It changes direction constantly | `StrafeTimeLimit` short with `bAutoStrafeSwap` on; raise `StrafeSwapThreshold` or `StrafeTimeLimit`. |
| A role's profile is ignored | The 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:

  ```python
  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:**

  ```python
  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:

```python
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:

```python
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.**

| Stimulus | Arrives when |
| --- | --- |
| `SEC.Stimulus.DamageTaken` | The enemy takes a hit (the duplicated demo Blueprint reports it). |
| `SEC.Stimulus.AttackTelegraphed` | A swing near it is announced by a SEC Telegraph Window on the attacker's montage. |
| `SEC.Stimulus.StanceBroken` | The 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

| Field | Does |
| --- | --- |
| `RuleId` | Name in logs. |
| `OnStimulus` | The stimulus tag it answers. |
| `RequireContextTags`, `BlockContextTags` | Tags 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. |
| `Conditions` | Gates that must all pass (below). |
| `ReactionLinks` (`unreal.SECReactionTriggerLink`) | `ReactionId`, `WeightMultiplier`. |
| `ReactionCategory` | Answer with any reaction of this category instead of named links. |
| `RulePriority` | Higher is asked first. |
| `Chance` | 0 to 1. |
| `MinInterval` | Seconds 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 when | Fields |
| --- | --- | --- |
| `USECGate_IncomingAngle` (Incoming Angle Gate) | The blow arrives inside an arc in front | `HalfAngleDegrees`, `bPassWithoutDirection`, `bInvert` (behind instead) |
| `USECGate_TimeToImpact` (Time To Impact Gate) | Enough time is left before an announced swing lands | `MinSeconds`, `MaxSeconds` |
| `USECGate_StimulusMagnitude` (Stimulus Magnitude Gate) | The blow is worth answering | `MinMagnitude`, `MaxMagnitude` |
| `USECGate_SourceDistance` (Source Distance Gate) | The attacker is in range | `MinDistance`, `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

```python
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

| Symptom | Cause and fix |
| --- | --- |
| Never reacts | No trigger rule, or the rule's `ReactionLinks` name an ID no reaction has. |
| Never parries | The 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 combo | Raise the rule's `MinInterval` or the reaction's `Cooldown.Duration`. |
| Back hits play the front reaction | Both 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:**

```python
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 1` | Every overlay above each enemy: awareness, brain, action, role, team |
| `SEC.Debug.WatchPawn <name>` | Narrows the overlays to one enemy |
| `SEC.Debug.Scoring 1` | Each action's score, and why a gate refused it |
| `SEC.Debug.LogActionDecisions 1`, `SEC.Debug.LogActionExecution 1` | Each pick, and each action starting and ending |
| `SEC.Debug.LogReactions 1` | Each trigger rule firing or skipped, with the reason |
| `SEC.Debug.LogRoleAssignments 1` | Role assignments |
| `SEC.Debug.Melee.DrawTracing 1` | Every melee sweep |
| `SEC.Debug.Movement.DrawScoring 1`, `SEC.Debug.Movement.DrawAvoidance 1` | Movement direction scores and avoidance |
| `SEC.Debug.Threat.Draw 1` | The player's look cone and the threat level |
| `SEC.Debug.Stimulus.Draw 1` | Announced swings and who they reach |
| `SEC.Debug.Warp 1` | Where each motion warp lands |
| `SEC.Logs Verbose` | Raises 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

| Symptom | Cause and fix |
| --- | --- |
| Never dies | No `SEC.Vital.Health` row, or `bAutoHandleDeathOnHealthDepleted` off. |
| The stance never breaks | No stance row on the enemy, or the attacks' damage configs drain none. |
| The stance empties but no stun plays | Nothing reports `SEC.Stimulus.StanceBroken` (the On Vital Depleted step above), or the Reaction Set has no rule on it. |
| The fight never starts | The 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 restart | It 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) | Does | Fields |
| --- | --- | --- |
| `USECGameplayCueHitEffect` (SEC Play Gameplay Cues) | Plays cues on the attacker and the target | `AttackerCues`, `TargetCues`, `SurfaceSet` (`USECImpactSurfaceSet`: cue suffix per physical material or surface) |
| `USECPhysicsImpulseHitEffect` (SEC Apply Physics Impulse) | Shoves the struck body | `HitBoneForce`, `HitOverallForce`, `HitRotationalForce`, `bVelocityChange` |
| `USECApplyGameplayEffectHitEffect` (SEC Apply Gameplay Effect) | Applies a gameplay effect | `GameplayEffect`, `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:**

```python
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) | Does | Fields |
| --- | --- | --- |
| `USECDefenseRule_Guard` (SEC Guard) | Soaks a hit arriving inside an arc | `ArcHalfAngleDegrees`, `DamageMultiplier` (0 blocks all) |
| `USECDefenseRule_Scale` (SEC Scale Damage) | Scales a hit | `DamageMultiplier` |
| `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:**

```python
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

| Symptom | Cause and fix |
| --- | --- |
| Hits pass through | `SocketIDs` on the window differ from the weapon's `TraceSockets` `ID`, or `bUseAsWeapon` is off on the loadout entry. |
| Enemies hit each other | The trace's `TeamFilter` is `NO_FILTER`; set `ENEMIES_ONLY`. |
| A stance or stamina drain does nothing | The target has no row for that vital. |
| The enemy never blocks | Nothing gives it `SEC.State.Blocking` while it guards, or the rule set is on neither the pawn's Defense component nor its weapon. |

---

Previous page: [Minimal Enemy](https://rigbak.com/docs/soulslike-combat/minimal-enemy). Next page: [Movement System](https://rigbak.com/docs/soulslike-combat/movement-system)
