# Combat Roles

> Coordinate a pack: cap how many enemies attack at once, and swap each enemy's moveset, reactions and movement to match the role it holds.

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

> Four enemies around one player: one pressing the attack, the rest circling and waiting their turn. Roles decide who is which, and swap each enemy's behavior assets to match.

## Set it up (60 seconds)

1. **Make the player a combat target**

   An enemy whose **Enemy AI Config** sets an **Awareness Config** adds the player to the target pool once it detects them, so with awareness set up this step happens on its own. With no Awareness Config, or with **Always Aware** ticked, register the player yourself: in the player pawn's **BeginPlay**, get the **AI Combat Role Subsystem** and call **Register Combat Target** with `Self`.

   `BP_SEC_PlayerCharacter` in the demo content makes that call, and `BP_SEC_Dummy` shows the same wiring on a stationary target.
2. **Drop three or more enemies into the level**

   Each enemy joins the pool when its AI controller possesses it. **Auto-Register for Combat Roles** on the **Enemy AI Config** is ticked out of the box, so this step is already done.

   In **EnemyConfig_Grunt**, under Combat Role, set:

   | Property | Value |
   | --- | --- |
   | Auto-Register for Combat Roles | true |
   | Allowed Roles (empty = any) | Empty |
   | Priority | 0 |

   *An untouched config already joins the pool and accepts any role.*
3. **Cap how many attack at once**

   Open **Project Settings**, **Plugins**, **Soulslike Enemy Combat**. Under **Role Limits**, set the **Attacker** row to 1 for the classic one-at-a-time fight. It ships at 3.
4. **Give the roles something to do**

   Open the **Enemy AI Config**. Under **Movement Profiles**, set **Default Movement Profile** to a patient profile, then add a **Role Movement Profiles** entry for `SEC.Role.Attacker` pointing at an aggressive one. The demo content ships five to start from, `MovementProfile_SEC_Attacker` through `MovementProfile_SEC_Elite`.

   In **EnemyConfig_Grunt**, under Movement Profiles, set:

   | Property | Value |
   | --- | --- |
   | Manage Movement Profiles Automatically | true |
   | Default Movement Profile | MovementProfile_SEC_Waiter |
   | Role Movement Profiles | 1 Array element (replaces 0 Array elements) |
   | Role Movement Profiles > [0] Role | SEC.Role.Attacker |
   | Role Movement Profiles > [0] Movement Profile | MovementProfile_SEC_Attacker |

   *The default covers every role that has no entry of its own, so one override is enough to see the difference.*
5. **Play, and watch the roles swap**

   Press Play and run `SEC.Debug.Role 1` in the console. Each enemy gets its name and role above its head, colored by role: red Attacker, orange Flanker, blue Waiter, green Supporter, purple Elite.

> **Warning**
>
> An enemy with no combat target holds no role. Role slots are counted per target, so an enemy the subsystem has not paired with one is skipped both when it registers and on the reassignment timer. If the roles above every head read empty, nothing registered the player. See [Getting Started](https://rigbak.com/docs/soulslike-combat/getting-started) and [Targeting](https://rigbak.com/docs/soulslike-combat/targeting-system).

---

## The six roles

Roles are gameplay tags under `SEC.Role`.

| Role | Tag | What it is for |
| --- | --- | --- |
| Attacker | `SEC.Role.Attacker` | Front-line pressure. The role the fight revolves around. |
| Waiter | `SEC.Role.Waiter` | Holds distance and waits for a slot to open. |
| Flanker | `SEC.Role.Flanker` | Circles for an angle. |
| Supporter | `SEC.Role.Supporter` | Ranged fire or support from the back. |
| Elite | `SEC.Role.Elite` | Boss-tier enemy. Unlimited slots out of the box. |
| None | `SEC.Role.None` | What an enemy gets when the pass finds no free slot for it. |

An enemy the pass cannot seat holds `SEC.Role.None` and fights with its AI Config's default sets. The deprecated **Get Fallback Role** node returns a setting that takes no part in that.

*Interactive diagram on the page: Enemies dynamically swap roles based on slot availability and priority.*

To add a role of your own, define a tag under `SEC.Role` in **Project Settings**, **Gameplay Tags**, then list it under **Additional Roles** in **Project Settings**, **Plugins**, **Soulslike Enemy Combat**. It is scored and filled alongside the built-ins.

---

## Cap how many hold each role

**Role Limits** in **Project Settings**, **Plugins**, **Soulslike Enemy Combat** is a list of role and limit pairs. A role with no row has unlimited slots.

| Role | Ships at |
| --- | --- |
| Attacker | 3 |
| Flanker | 2 |
| Supporter | 2 |
| Waiter | unlimited |
| Elite | unlimited |
| None | unlimited |

Each combat target counts its own slots, so two players can each field three Attackers.

> **Note**
>
> Role limits cap how many enemies **hold** a role. To cap how many **swing** at the same moment, reach for [Combat Tokens](https://rigbak.com/docs/soulslike-combat/combat-tokens), which gate one action rather than an enemy's whole behavior set. The two stack.

---

## Give each role its own behavior

When an enemy's role changes, its controller resolves three assets and applies them.

| Per-role list | Falls back to | Master switch |
| --- | --- | --- |
| **Role Action Sets** | **Default Action Set** | Manage Action Sets Automatically |
| **Role Reaction Sets** | **Default Reaction Set** | Manage Reaction Sets Automatically |
| **Role Movement Profiles** | **Default Movement Profile** | Manage Movement Profiles Automatically |

All three live on the **Enemy AI Config**. A role with no entry in a list takes that category's default. An equipped weapon's own sets beat the config's role entries, so a bow-carrying skeleton keeps its archer moveset whatever role it holds. See [Actions](https://rigbak.com/docs/soulslike-combat/action-system), [Reactions](https://rigbak.com/docs/soulslike-combat/reaction-system), [Movement](https://rigbak.com/docs/soulslike-combat/movement-system).

> **Warning**
>
> Fill in the default as well as the role list. An enemy holding a role you did not list, or no role at all, gets whatever the default names; leave the default empty and it gets nothing.

---

## Steer which enemy gets which role

These sit under **Combat Role** on the **Enemy AI Config**.

**Category: Combat Role**

| Property | Value |
| --- | --- |
| Auto-Register for Combat Roles | true |
| Allowed Roles (empty = any) | no tags |
| Priority | 0 |
| Preferred Role | None |
| Fitness Evaluators | 0 Array elements |
| Target Selector | None |
| Target Filter | None |
| Ignore Target Redistribution | false |

| Field | What to do with it |
| --- | --- |
| **Auto-Register for Combat Roles** | Untick it to keep this enemy out of the pool. It takes no slot and holds no role, and its controller still applies the config's default action set, reaction set and movement profile at possession. |
| **Allowed Roles (empty = any)** | Empty lets the enemy hold any role. List tags to pin an archer to Waiter and Supporter so it is not sent to the front. |
| **Priority** | Higher values get first pick of limited slots. 0 for a grunt, 50 for an elite, 100 for a mini-boss, 200 for a boss. |
| **Preferred Role** | Adds 0.05 to that role's fitness score, enough to break a tie. It must also appear in Allowed Roles, unless that list is empty. |
| **Fitness Evaluators** | Empty scores on distance alone, closest first, out to 2000 cm. See the drawers below. |
| **Target Selector** | Which registered target this enemy fights when several exist. See [Targeting](https://rigbak.com/docs/soulslike-combat/targeting-system). |
| **Target Filter** | Which actors this enemy will fight at all, asked before any target is selected. See [Targeting](https://rigbak.com/docs/soulslike-combat/targeting-system). |
| **Ignore Target Redistribution** | Pins this enemy to its target while the rest of the pack redistributes. Use it for a boss. |

---

## How often roles change

**Category: Timing**

| Property | Value |
| --- | --- |
| Role Reassignment Interval | 8 |
| Min Time in Role | 8 |
| Reevaluate Targets on Reassignment | false |

| Field | Default | What to do with it |
| --- | --- | --- |
| **Role Reassignment Interval** | 8 s | How often the whole pool is re-scored. Lower it for a fight that reshuffles quickly. Set it to 0 to stop the timer and drive reassignment by hand. |
| **Min Time in Role** | 8 s | Hysteresis. An enemy still inside this window gets 0.3 added to its current role's score on every evaluation, a change of target included, which keeps it where it is unless another role fits it by more than that. Lower it for more churn. |
| **Reevaluate Targets on Reassignment** | off | Re-runs each enemy's Target Selector before roles are scored. Needs two or more registered targets; it does nothing with one. |

A newly registered enemy does not wait for the timer. It takes the best free slot the moment it registers, without disturbing anyone else. An enemy moved to another target does the same there, without its own role counting against the new target's limit, and **Min Time in Role** still adds its 0.3 to the role it holds.

---

## Debug it

```
SEC.Debug.Role 1                  // Name and role above each enemy, colored by role
SEC.Debug.LogRoleAssignments 1    // Each role change, with the fitness score behind it
SEC.Debug.LogRoleAssignments 2    // Adds every enemy-to-role pair the pass considered
```

`SEC.Debug.All 1` turns every overlay section on at once, and `SEC.Debug.WatchPawn <name>` narrows them to one enemy. The role line replicates, so it reads on a client as well as on the server.

The **SEC Combat Role** component on the pawn carries a **Debug Overlay Role** checkbox that draws the same line for that one enemy without touching the console.

<details>
<summary>Nothing changes role (Advanced)</summary>

In the order worth checking:

- **No registered combat target.** Slots are counted per target, so an enemy paired with none is skipped by registration and by the timer alike. This is the common one and it logs at Verbose.
- **Auto-Register for Combat Roles is unticked** on the config.
- **The controller has no SEC Combat Controller component.** That component is what calls **Register Combatant** at possession, so an enemy without it never joins the pool. Calling **Register Combatant** yourself succeeds either way, on default parameters: any role, priority 0, no evaluators. **Enemy Controller Base** ships with the component.
- **Only one enemy is in the fight.** With default limits it takes Attacker and stays there. Roles move when there are more enemies than slots.
- **Role Reassignment Interval is 0.** The timer auto-starts only when the interval is above 0. Call **Force Reassignment** yourself, or **Start Reassignment Timer** after setting an interval.
- **Reassignment is paused.** **Pause Reassignment** freezes role changes until **Resume Reassignment**.
- **The role is locked.** **Force Assign Role** with its lock argument set makes the evaluator skip that enemy. **Unlock Role** clears it.
- **The enemy is mid-action.** An enemy whose action is executing counts as locked for that pass, keeps its role and holds its slot. Cap Attacker at 1, give it a long attack, and the pool reads frozen from the outside.
- **Min Time in Role is long against the interval.** The hysteresis bonus then holds a role through most passes.

An enemy reading `SEC.Role.None` was scored and found no free slot. Before its first assignment the tag is empty rather than None.

</details>

<details>
<summary>Score the fit yourself (Advanced)</summary>

Leave **Fitness Evaluators** empty and role fitness comes from distance alone, closest scoring highest, out to 2000 cm. Add evaluators to that list to score on anything else. Several combine into a weighted average.

Three fields sit on each one:

- **Role Influence Weights** maps a role tag to how strongly this evaluator counts for it: 1.0 full, 0.5 half, 0.0 not at all.
- **Influence On Unlisted Roles** (0) is what a role missing from that map gets.
- **Score Mode** reads the raw score as-is (*Higher Score = Better Fit*) or inverts it (*Lower Score = Better Fit*).

### Distance Evaluator

Scores how far the enemy stands from its assigned target. The score peaks at 1 at **Ideal Distance** and falls off linearly to 0 at **Ideal Distance** plus or minus **Effective Range**. It ships weighted onto Attacker.

**Category: Affected Roles**

| Property | Value |
| --- | --- |
| Role Influence Weights | SEC.Role.Attacker → 1.0 |
| Influence On Unlisted Roles | 0 |

**Category: Scoring**

| Property | Value |
| --- | --- |
| Score Mode | Higher Score = Better Fit |

**Category: Distance Settings**

| Property | Value |
| --- | --- |
| Ideal Distance | 0 |
| Effective Range | 2000 |

- Closest enemies become Attackers: **Ideal Distance** 0, **Effective Range** 2000, *Higher Score = Better Fit*, weights `{ Attacker: 1.0 }`.
- Farthest become Waiters: the same distances, *Lower Score = Better Fit*, weights `{ Waiter: 1.0 }`.
- Mid-range enemies become Flankers: **Ideal Distance** 1000, **Effective Range** 500, *Higher Score = Better Fit*, weights `{ Flanker: 1.0 }`.

### Cooldown Evaluator

Scores how much of the enemy's moveset is on cooldown, so a spent Attacker rotates out and a fresh one steps in.

**Category: Affected Roles**

| Property | Value |
| --- | --- |
| Role Influence Weights | 0 Map elements |
| Influence On Unlisted Roles | 0 |

**Category: Scoring**

| Property | Value |
| --- | --- |
| Score Mode | Higher Score = Better Fit |

**Category: Cooldown Settings**

| Property | Value |
| --- | --- |
| Current Role Penalty | 0.5 |

It answers 0.5, a neutral score, whenever the role the enemy currently holds is missing from **Role Influence Weights**. List the roles you want to fatigue (Attacker, Flanker) and leave the passive ones out. **Current Role Penalty** (0.5) multiplies the score while the pass considers keeping the enemy in the role it already holds, so a lower value pushes harder for a swap.

</details>

<details>
<summary>Writing your own evaluator (Advanced)</summary>

Subclass **Role Evaluator** in Blueprint or C++ and override **Evaluate Fitness**, returning 0 for unfit and 1 for a perfect fit. The subsystem applies **Score Mode** and the per-role weights afterward, so return the raw number.

The context hands you four things:

| Field | Use |
| --- | --- |
| **Controller** | Reach the pawn and its components from here. |
| **Role** | The role under evaluation. |
| **Current Role** | The role the enemy holds right now. |
| **Time in Current Role** | Seconds it has held that role. |

Get the assigned target with **Get Combatant Target** on the subsystem. Return 0.5 when your criterion does not apply.

</details>

<details>
<summary>Bosses and enemies that skip coordination (Advanced)</summary>

| Goal | Setting |
| --- | --- |
| Skip slot coordination entirely | Untick **Auto-Register for Combat Roles** on the Enemy AI Config. |
| Stay on one player while the pack redistributes | Tick **Ignore Target Redistribution**. |
| Script a fixed role | **Force Assign Role** with its lock argument set. |
| Wear a role tag without consuming a slot | **Apply Local Combat Role** on the SEC Combat Controller component. |

**Apply Local Combat Role** resolves and applies the action set, reaction set, weapon sets and movement profile for a role, and publishes the tag for clients, without joining the coordination. An enemy still registered with the subsystem has it overwritten on the next pass, so pair it with an unregistered enemy.

An unregistered enemy wires its own sets through the config defaults and consumes no slots, which leaves the pack's Attacker cap for the enemies that need it.

</details>

<details>
<summary>Several players, several pools (Advanced)</summary>

Each registered target keeps an independent pool with its own slot counts. Register a second player and the two pools fill separately.

| Function | Does |
| --- | --- |
| **Register Combat Target** | Adds the actor to the pool. **Auto Assign Unassigned** evaluates roles straight away instead of waiting for the timer, at most once a frame for a target already in the pool, so a whole group alerted at once costs one evaluation; **Reevaluate Existing** also re-runs every assigned enemy's Target Selector. |
| **Unregister Combat Target** | Removes it. Enemies left behind lose their target, reset to `SEC.Role.None`, and re-run their own Target Selector across the remaining targets. One whose selector answers nothing stays unassigned. |
| **Assign Combatant to Target** | Moves one enemy to a pool. This one carries out the order as given, without asking the enemy's Target Filter. |
| **Reassign Combatant Target** | Re-runs one enemy's Target Selector. |
| **Reevaluate All Targets** | Re-runs every assigned enemy's Target Selector. Honors **Ignore Target Redistribution**. |
| **Transfer Combatants to Target** | Hands one pool over to another target, for a player death handoff. |
| **Balance Combatants Across Targets** | Round-robins the pack evenly, ignoring Target Selectors. |
| **Set Primary Target and Assign All** | Puts the whole pack on one target, for a boss or a VIP. |
| **Force Reassignment for Target** | Re-scores one pool instead of all of them. |
| **Get Role Count for Target**, **Get Combatants with Role for Target** | Per-pool slot usage. |

Team rules still apply. An enemy whose own Target Filter turns a target aside keeps what it had rather than being handed something it will not fight.

</details>

<details>
<summary>Reading the role on a client (Multiplayer)</summary>

Role evaluation runs on the server, and AI controllers do not exist on clients. The **SEC Combat Role** component on the pawn replicates the tag so a nameplate, an icon or a debug overlay can read it anywhere.

**Details panel: SEC Combat Role** (component, in AEnemyCharacterBase)

**Category: AI > Combat Role**

| Property | Value |
| --- | --- |
| Combat Role | SEC.Role.Attacker |

*Replicated runtime state on the pawn. SECCombatControllerComponent calls SetCombatRole on the server; clients bind OnCombatRoleChanged.*

**Enemy Character Base** adds the component by default. Read **Get Combat Role**, or bind **On Combat Role Changed**, which fires on the server and on every client. **Set Combat Role** is server only; the controller calls it, and calling it anywhere else changes what clients see and nothing more, since the behavior assets resolve on the controller.

</details>

<details>
<summary>Driving roles from your own controller (Alternative)</summary>

A custom AI controller with no **SEC Combat Controller** component still receives the role change on the global delegate, but nothing swaps its assets. Bind **On Combat Role Changed** and sync by hand through the **SEC Combat Role Sync Library**:

| Function | Syncs |
| --- | --- |
| **Sync All For Combat Role** | Action set, reaction set, the sets the equipped weapon gives, and the movement profile. |
| **Sync Action Set For Role** | The action set alone. |
| **Sync Reaction Set For Role** | The reaction set alone. |
| **Sync Movement Profile For Role** | The movement profile alone. |

Each takes the new role tag and finds the components it needs on the pawn and the controller. **Sync All For Combat Role** and **Sync Movement Profile For Role** also take the resolved **Enemy AI Config**; the action set and reaction set functions take the pawn and the role tag alone.

</details>

<details>
<summary>Changing the rules at runtime (Advanced)</summary>

Every function below sits on the **AI Combat Role Subsystem** and is callable from Blueprint.

**Slots and timing**

| Function | Does |
| --- | --- |
| **Set Role Limit Override** | Caps a role for this session. `-1` is unlimited, `0` disables the role. **Clear Role Limit Override** and **Clear All Role Limit Overrides** put the project settings back. |
| **Set Reassignment Interval** | Seconds between passes. 0 stops the timer. |
| **Set Min Time in Role** | The hysteresis window. |
| **Set Reevaluate Targets on Reassignment** | Target re-pick before each pass. |
| **Update Config** | Bulk update. With attrition on, existing holders keep their roles when a limit shrinks instead of being stripped. |
| **Reset to Default Settings** | Drops every runtime override. |

**Assignment and queries**

| Function | Does |
| --- | --- |
| **Force Reassignment** | Re-scores every pool now. |
| **Pause Reassignment**, **Resume Reassignment** | Freeze role changes through a cutscene. |
| **Start Reassignment Timer**, **Stop Reassignment Timer** | Timer control. **Start Reassignment Timer** logs a warning and returns while the interval sits at 0, so set an interval first. |
| **Force Assign Role**, **Unlock Role**, **Is Role Locked** | Scripted roles. |
| **Get Role**, **Get Role Count**, **Get Combatants with Role** | Current assignments. |
| **Is Registered**, **Get All Combatants**, **Get Total Combatant Count** | Pool membership. |

An enrage phase that opens a slot is **Set Role Limit Override** on the Attacker tag followed by **Force Reassignment**, so the extra attacker steps in on the same frame rather than at the next 8 second pass.

</details>

<details>
<summary>Events you can bind (Advanced)</summary>

| Event | Fires when |
| --- | --- |
| **On Combat Role Changed** | Any role change. |
| **On Combat Role Changed for Target** | The same, carrying which target the enemy is assigned to. |
| **On Combatant Registered**, **On Combatant Unregistered** | An enemy joins or leaves the pool. |
| **On Combat Target Registered**, **On Combat Target Unregistered** | A target is added or removed. |
| **On Combat Target Changed** | A primary-target handoff. |
| **On Combatants Orphaned** | A target was lost, carrying the enemies left behind. Their roles reset to `SEC.Role.None`; reassign them here. |

</details>

---

Previous page: [Telegraphs](https://rigbak.com/docs/soulslike-combat/attack-telegraphs). Next page: [Combat Tokens](https://rigbak.com/docs/soulslike-combat/combat-tokens)
