Questlight Studio
Docs / Enemy AI and adaptive difficulty

14. Enemy AI and adaptive difficulty

In short: Every enemy (and every companion nobody plays) fights with an AI profile: a small data asset that says what the unit goes after. One utility AI serves every battle mode. A single number per mode, the intelligence (0 clumsy, 1 sharp), says how well it does it, and adaptive difficulty moves that number slowly from how the player is doing. Boss phases, team roles, rules and scorers make special enemies without code. If you already have Behavior Trees or StateTrees, nodes let them ask the utility AI what to do.

14.1 Basics

14.1.1 The idea: profile plus intelligence

  • The profile (URSEAIProfile, assets named DA_AI_*) is what the unit wants: damage, kills, healing, statuses, safety, distance, protecting allies.
  • The intelligence is how well it pursues that. At low values it sometimes picks the second or third best move (plausible mistakes, never stupid ones), forgets weaknesses, ignores danger and rarely coordinates. At high values it uses weaknesses, focuses fire, guards its team, anticipates and (in the live modes) answers telegraphs with a well-timed dodge.
  • The player never chooses a difficulty. The game starts at "Medium" (0.5) and adapts the enemies' intelligence only, never their stats or the rewards. Player progress (level, gear) always counts.

The same profile drives grid Tactics, Free Tactics, ATB, turn-based and Action battles. Each mode has an adapter that turns the shared scoring into its own commands.

14.1.2 Pick a ready style

  1. RPG Saga Studio → New enemy, step Abilities and AI: pick a style card. Each card has a one-line description.
  2. Want something else? Studio → New AI style (or Configurator → Tools → AI Profile Preview → New AI style...):
    1. pick a base style (the Boss tab lists the boss styles);
    2. name it, choose its team role and move the sliders;
    3. check the curves over intelligence;
    4. Simulate: your starting party fights test enemies that play the new style, then the base style;
    5. Create. The profile appears in the New Enemy wizard's list.

The ready styles:

StyleRoleWhat it does
BerserkerDamageCharges the nearest foe and never backs off; hits harder the more hurt it is
SniperDamageShoots from as far as it can and backs away from anyone who closes in
SummonerSupportCalls its creatures first and stays safely behind them
TankTankStands between enemies and its fragile allies, taunts, guards, blocks corridors
ControllerControllerSleeps, slows and weakens before dealing damage; casters and healers first
CowardNoneWeak hitter that runs away when hurt
HealerSupportKeeps allies alive: raises and heals first, buffs, stays out of reach
SkirmisherDamageDarts in to strike from the side, then pulls back
AmbusherDamageHolds position until a foe is within reach, then strikes hard
Pack HunterDamageEvery member goes for the same prey until it falls

Older profiles (Aggressive, Cautious, Guardian, Hunter, Support, Trickster) stay available. Boss styles are a profile plus phases: Juggernaut, Summoner Lord, Phase Shifter, Glass Cannon Caster, Enrage Timer and Duelist. They use only plugin content, so they work in an empty project. Replace the abilities and summons with yours.

14.1.3 Bosses and phases (no code)

A boss profile has Phases, entered in order at safe points (before any AI decision). In the AI Style wizard's Boss tab each step is one phase:

  • When: HP below a percentage, an own turn count, a number of allies down, or custom conditions.
  • Then: a banner and a line, statuses gained or removed, abilities gained or lost, units that join (count, how many turns), a new behavior (profile), a second wind (HP restored to a percentage), a cinematic.

Everything else of a phase (extra triggers, rules) stays editable in the profile. Create writes DA_AI_<Name> in your project. Creating again with the same name updates the profile in place, so enemies using it keep it. Plugin styles are never changed.

14.1.4 Set per-encounter limits

On an Encounter asset, Min Intelligence and Max Intelligence bound what adaptive difficulty may do for that fight. A boss with Min Intelligence 0.7 never drops below 0.7, however poorly the player does. To pin a fight at one value, set both to it.

14.2 How it works

Battle starts
  -> URSEDifficultySubsystem::BeginBattle resolves the intelligence for the mode and encounter
  -> each AI turn: the mode's adapter builds options (abilities x targets x positions)
  -> the shared utility brain scores them: profile weights (curves over intelligence) + rules + scorers + role terms
  -> "decision quality" noise: at low intelligence the 2nd or 3rd best option may win
  -> the Decision Policy (optional Blueprint) may override the choice
  -> the mode turns it into a command (Free Tactics, Tactics, ATB, turn-based or Action)
Battle ends
  -> URSEDifficultySubsystem::RecordBattle measures it and nudges the intelligence for that mode

14.2.1 Utility scoring

Every option gets a score from the profile's weights. A weight is an FRSEAIScalar: a constant, a range over intelligence (Min at 0, Max at 1) or a curve. Weights include damage, kill bonus, flanking, status value, healing, danger avoidance, preferred distance, protecting allies, retreat HP, resource cost, and the intelligence knobs: Decision Quality, Weakness Knowledge, Danger Awareness, Focus Fire, VIP Focus, Pact Awareness, Charge Interrupt and Resource Management. Gates switch whole behaviors (flanking, protecting the support, combos) on only from a given intelligence. Global numbers (the noise temperature at low and high intelligence, focus-fire weight, item use penalty, summon value) are in Project Settings → AI.

14.2.2 Team roles

A profile's Role gives the unit team behavior in every mode:

RoleBehavior
TankHolds the line: stands on the enemy's way to a fragile ally (support, controller, VIP, a healer, a badly hurt ally), prefers taunts and guards allies the enemy can reach
DamageGoes for kills (a likely kill is worth more) and high-value targets: dangerous enemies, casters, healers
ControllerDisables and debuffs before raw damage, casters and healers first, areas that catch several enemies
SupportHeals, plus buffs on allies enemies can reach; the others guard it

Only the weights of the unit's own role apply (the Team Role category of the profile).

14.2.3 Rules and scorers

  • Rules (FRSEAIRule, "gambits") are ordered if-then entries evaluated inside the pipeline. When every condition holds they Force an action, add a score Bonus or Forbid abilities. Each can apply only within an intelligence band.
  • Scorers (URSEAIScorer) are extra considerations added to every ability option: weight times a raw value. Built in: Target Status, Target Missing HP, Target Threat, Target Is Caster Or Healer, Preferred Range, Ally Needs Protection, Area Value and Setup Combo.
  • Conditions (URSEAICondition) are small tests used by rules and phases: Health, Resource, Attribute, Weak To Element, Has Status, Enemies In Range, Unit Count, Turn Number, Phase, Intelligence, Target Is Focus, Not, Any Of, and use conditions such as Has Tag, Equipped, Row, Position, Identity, Battle Mode and Story.

14.3 Key classes and assets

Class / assetModuleRole
URSEAIProfileRSECoreThe AI profile data asset: weights, gates, role, rules, scorers, phases, last stand, decision policy
FRSEAIScalarRSECoreA value that may change with intelligence (constant, range or curve)
FRSEAIRuleRSECoreOne if-then rule
FRSEAIPhaseRSECoreOne boss phase
URSEAICondition, URSEAIScorerRSECoreAbstract, Blueprintable extension points
URSEAIDecisionPolicyRSECoreAbstract, Blueprintable: the last word on every decision
URSEAIBattleBrainRSECoreThe AI state of one battle: phases, team blackboard, debugger records
URSEFreeTacticsAIController, URSEActionAIControllerRSECoreMode controllers for Free Tactics and Action
URSEAISettingsRSECoreGlobal AI numbers (Project Settings → AI), a rules-profile section
URSEDifficultySubsystemRSECoreGame-instance subsystem: owns the adaptive state, resolves intelligence, records battles
URSEDifficultySettingsRSECoreProject Settings → Difficulty
URSEAIStyleLibraryRSECoreBlueprint/Python helpers behind the AI Style wizard
URSEAIInteropRSEShared logic of the Behavior Tree and StateTree nodes
URSEActionInterop, URSEActionAgentsRSEAction battle side of the interop, and finding an actor's action agent
URSECombatRoleRegistry, URSECombatRoleSettingsRSECoreWhat your props mean in battle (cover, obstacles, ...)

14.4 Settings

WhereWhat
Project Settings → RPG Saga Engine → AICandidate limits (Max Move Candidates), noise (Noise Top Options, temperature at min and max intelligence), danger awareness floor, focus fire, VIP multiplier, ATB charge interrupt, item penalties, revive and summon value
… → Action AIReal-time numbers: think rate, positioning, reactions, party orders (see Action battles)
… → Free Tactics → AICandidate positions, cover, height, spacing, overwatch and opportunity risk weights, single move, jumps
… → DifficultyThe adaptive controller (below)
… → AI DebuggerThe in-game debugger
… → Combat RolesRoles, suggestions, registries

14.4.1 Adaptive difficulty settings

Tactics and ATB each have their own intelligence and their own history, so an ATB fight never moves the Tactics value (the turn-based modes use the ATB value).

SettingDefaultMeaning
EnabledonOff: the encounter's own intelligence is used and nothing is recorded
Tactics / ATB → Start Intelligence0.5 / 0.5"Medium" at a new game
Tactics → Target / Worst Turns Per Enemy4 / 10Tempo that counts as "even" and as "struggling"
ATB → Target / Worst Turns Per Enemy3 / 8The same for ATB
Min / Max Intelligence0.15 / 1Global limits
Window Size5Battles averaged per mode
Min Battles To Adjust3Battles needed before the first change
Gain0.2Speed of adjustment
Dead Zone0.05Distance from "even" that changes nothing
Max Step Up / Down0.05 / 0.03Largest change per battle; falling is slower than rising
Tempo / Damage / Fallen Weight1 / 1 / 1Weights in the battle score
Target / Worst Damage Taken Share0.35 / 1Share of the party's max HP lost
Target / Worst Fallen Share0.1 / 0.75Share of units that fell
Retry Penalty0.1Score lost per retry of a won fight
Retry Weight Falloff0.5Weight of repeated defeats at one encounter (1, 0.5, 0.25...)
Record EscapesoffWhether fleeing counts

How it works: after each battle the subsystem scores it from 0 (the player struggled) through 0.5 (even) to 1 (the player dominated), from turns per enemy, damage taken and units fallen. A defeat scores 0. The controller is relative: it moves the intelligence only while the average of the last few battles sits above or below 0.5, so a player who plays an even game at 0.8 stays at 0.8. Repeated losses at one encounter count less and less, so losing on purpose does not fool it.

The intelligence a battle actually uses comes from, in order: the RSE.Difficulty.Override console variable (0 to 1, debug), else the adaptive value for the mode clamped to the global limits and then the encounter's Min and Max (the encounter's own Intelligence field is ignored while adaptive difficulty is on), else the encounter's Intelligence when adaptive is off. Rules profiles can override the Difficulty section per zone or boss.

14.5 Blueprint usage

14.5.1 Make a "royal guard" with a Decision Policy

A Decision Policy has the last word on a unit's decision, after the utility scored and picked an option.

  1. Create a Blueprint with the parent AI Decision Policy.
  2. Override Override Decision: you get the context, the options (best first: kind, ability, item, target, score) and the index the AI picked. Set Out Index and return true to take another option.
  3. Override Override Target: when the final option uses an ability, you get every unit that ability was scored on. Return true with another Out Target Id to aim it there.
  4. Set it on the profile (Decision Policy). Keep it deterministic: read the battle, never change it, no global random. Rules marked Force are not sent to the policy.

Use the RPG Saga | AI nodes to read the battle: Get Unit, Get HP Percent, Has Status, Get Resource, Get Attribute, Is Weak To, Get Distance, Are Enemies, Count Units, Get Team Focus Target, Get Unit Ids, Evaluate Scalar.

For a "royal guard" that never attacks the player's leader while the queen lives: in Override Target, if the candidate is the VIP and a Queen team value is set, return another candidate.

14.5.2 Add your own scorer or condition

  • Parent AI Scorer, override Score (Context → float). Add it in the profile's Scorers with a Weight.
  • Parent AI Condition, override Is Met (Context → bool). Use it in rules and phases.

14.5.3 Read or set intelligence

On the difficulty subsystem: Get Mode Intelligence, Set Mode Intelligence and Get Intelligence For Encounter.

14.5.4 Change a unit's behavior mid-fight

A battle script effect Change AI Profile (URSEBattleScriptEffect_ChangeAIProfile) swaps the profile from a scripted beat. Boss phases do the same through their Profile field.

14.6 C++ usage

A custom scorer:

UCLASS(meta = (DisplayName = "Prefer Burning Targets"))
class UMyBurningScorer : public URSEAIScorer
{
	GENERATED_BODY()

protected:
	virtual float Score_Implementation(const FRSEAIContext& Context) const override
	{
		FRSEBattleUnit Target;
		if (!URSEAIBlueprintLibrary::GetUnit(Context, Context.TargetId, Target)) { return 0.f; }
		return URSEAIBlueprintLibrary::HasStatus(Context, Context.TargetId, MyTags::Status_Burning) ? 1.f : 0.f;
	}
};

Read the adaptive value from the game-instance subsystem:

if (URSEDifficultySubsystem* Difficulty = GetGameInstance()->GetSubsystem<URSEDifficultySubsystem>())
{
	const float Tactics = Difficulty->GetModeIntelligence(RSEGameplayTags::Battle_Mode_Tactics);
	UE_LOG(LogTemp, Log, TEXT("Tactics intelligence %.2f"), Tactics);
}

The state (FRSEDifficultyState) is SaveGame data: each mode's value and window, plus retries per encounter. It is part of the save (see Save and load). ResetToNewGame() puts everything back to Medium.

Extension points: subclasses of URSEAICondition, URSEAIScorer and URSEAIDecisionPolicy (C++ or Blueprint), the URSEFreeTacticsController for a fully custom Free Tactics decision-maker, and the interop below.

14.7 Advanced

14.7.1 Combat roles: what your props mean in battle

Battles run on your real map. A thick tree trunk should protect a unit behind it, a bush should not, a pebble should not matter and a pile of buckets should not end up in the middle of a fight. Combat roles tell the engine what each object means. You set them once per asset (a folder at a time if you like), and every placed copy follows. Nothing in your asset packs is modified: roles live in a registry asset in your project (/Game/Generated/DA_CombatRoles), listed in Project Settings → Combat Roles → Registries.

RoleIn battle
Full coverBlocks line of sight and projectiles; occupies its tiles
Half coverAttacks against a unit behind it hit less often; occupies its tiles
ObstacleCannot be crossed, protects nobody
Difficult terrainCan be crossed, costs more movement
Climbable / high groundUnits stand on it and get the height advantage
Destructible coverHalf cover that can be broken (Free Tactics gives it hit points)
ExplosiveHalf cover that explodes when broken
OverheadLooked through; the Free Tactics camera cuts it away
Neutral decorationDoes not matter; the grid looks through it
Avoid in combat areasSmall clutter that placement tools keep out of combat areas

Quick start:

  1. In the Content Browser, right click an asset-pack folder → RPG Saga: Suggest Combat Roles (Unassigned Only). Every mesh and Blueprint gets a suggestion from its shape and its name.
  2. Right click the folder → RPG Saga: Combat Roles... to review the list in the Combat Role Configurator. Check rows, pick a Role, Apply to Checked. Manual choices always win over suggestions.
  3. Press Bake Grid again on Tactics zones (roles are read at bake time).

One placed object can differ from its asset: add the RSE Combat Role component to the actor, or an actor tag named like the role tag (for example CombatRole.Neutral). Place an RSE Combat Area over spots where you want battles and press Check to count free tiles and list clutter. In Blueprint, the node group RPG Saga | Combat Roles has Get Component Role, Get Actor Role, Get Asset Role, Get Cover Info, Is In Combat Area and Can Place At. Roles are data: add your own in Project Settings → Combat Roles → Roles with a new CombatRole. gameplay tag.

14.7.2 Behavior Tree and StateTree interop

Already have enemies, companions or NPCs run by a Behavior Tree or a StateTree? Keep them. Nodes for both let your trees ask the utility AI what to do, carry the answer out, keep a distance, start a battle and test the plugin's conditions. Movement, animation and combat code stay yours. Behavior Tree nodes are built into the RSE module; StateTree nodes are in the RSEStateTree module (enable RPG Saga Engine - StateTree Bridge in Edit → Plugins).

What it doesBehavior TreeStateTree
Asks the utility AI what the pawn should dotask RSE: Pick Best Actiontask RSE: Pick Best Action
Carries an action outtask RSE: Execute Abilitytask RSE: Execute Ability
Walks to a distance from a targettask RSE: Move To Preferred Rangetask RSE: Move To Preferred Range
Starts a battle with the partytask RSE: Start Battle With Targettask RSE: Start Battle With Target
Runs an Interaction asset(your own task)task RSE: Run Interaction
Tests plugin conditionsdecorator RSE: Conditioncondition RSE: Condition, RSE: Flag Is
Is the pawn in a battle / its turndecorator RSE: In Battlecondition RSE: In Battle
Does a unit have a statusdecorator RSE: Has Statuscondition RSE: Has Status
Tracks target, threat, distanceservice RSE: Update Threat / Targetevaluator RSE: Update Threat / Target

For action battles there are nodes of their own, with the same names in both systems: RSE Action: Attack, RSE Action: Use Ability, RSE Action: React To Threat, RSE Action: Keep Preferred Range, RSE Action: Target In Reach (decorator or condition) and RSE Action: Incoming Telegraph (decorator or condition).

Behavior Tree setup. Make a Blackboard with the keys you want (Target Actor as Object of base class Actor; Kind as Name; Ability as Object of base class RSE Ability Data; Item as Object of base class RSE Consumable Data; Target Location as Vector; Door Index as Int; Score, Threat and Distance as Float; In Battle as Bool). In your tree, pick the keys in each node's Output and Input groups. Every key is optional. BT_RSE_ExampleEnemy with BB_RSE_ExampleEnemy is in the plugin's example content. Its branches, from the top: fight on its turn (RSE: In Battle with Require Own Turn → Pick Best Action → Execute Ability), hunt the party (Move To Preferred Range → Start Battle With Target) and wait. If it is missing, run RSE.AIInterop.CreateExampleBT in the editor.

StateTree setup. Use the StateTree AI Component schema or the StateTree Component schema. Bind Pick Best Action's Output properties (Kind, Ability, Item, Target Actor, Target Location, Door Index, Score) to the inputs of Execute Ability. ST_RSE_ExampleEnemy is the example; RSE.StateTree.CreateExample creates it if missing.

Who decides. The battle stays the judge: whatever a tree does in a battle is a command like any other, validated and applied by the battle (the same in every mode and in the simulator). In Free Tactics the enemy side is played by the battle's own AI controller, and trees read and act around it. In Grid Tactics and ATB a tree can play a whole side: give it to an AI controller with no pawn and call Claim Battle Side (library RSE AI Interop, with the controller and the side's team tag). Release Battle Side hands it back. In action battles each unit is either the framework's or your pawn's own brain.

Your pawn in an action battle. Implement the RSE Action Agent interface (Class Settings → Interfaces) on your pawn or a component:

FunctionWhat it does
Can Accept Action OrderThe pawn can take an order now
Order Move To (Location, Acceptance Radius)Walk or run there; return true when you took it
Order Use Ability (Ability, Target Actor, Target Location)Play the ability with your animation and code
Order StopCancel the order
Is Busy With OrderAn order is still being carried out
Get Action Battle ControlFramework (our action AI plays the unit, the default) or Own Brain (your tree plays it)
On Action Battle Joined / On Action Battle LeftOptional notifications

To drive a boss with your own Behavior Tree: make the boss a Character with an AI controller, add the interface and return Own Brain, start the Action encounter passing your pawn in Enemy Actors, copy BT_RSE_ExampleBoss from the example content and run it. It answers the telegraph (RSE Action: Incoming Telegraph → React To Threat), strikes when in reach (Target In Reach → Attack) and closes in (Keep Preferred Range with Use Ability Reach). Add your own phase decorator above Strike and put Use Ability with the boss's special move in it.

The action nodes act only on the machine that hosts the battle; on a guest they read and do nothing. Decisions happen when a node runs, never every frame, and use the battle's seed, so the same state gives the same decision.

Without a tree. The library RSE AI Interop offers Is Actor In Battle, Is Actors Turn, Actor Has Status, Pick Best Action For (returns a decision without doing anything: good for hints) and Execute Decision.

14.7.3 Debugging the AI

  • RSE.AI.Debug 1 (or the AI button of the battle HUD): every decision with its top options and score terms (for example Role: kill focus, Role: taunt, Scorer: <name>). The debugger also notes when a Decision Policy changed a decision.
  • RSE.Difficulty.Override <0-1> fixes the intelligence of every battle (-1 turns it off). RSE.Difficulty.Log 1 logs every recorded battle (metrics, score, weight, change).
  • The battle simulator prints an enemy AI: line with behavior statistics. See Battle fundamentals.
  • Configurator → Tools → AI Profile Preview shows the curves, gates, rules, scorers and phases of a profile.

14.7.4 Performance notes

Decisions are sampled and capped: Max Move Candidates in AI settings, AI Candidate Positions in Free Tactics, a think interval in Action AI. Weights resolve once per decision, so curves cost nothing inside the scoring loop. Lower the caps for weak hardware.

14.8 Troubleshooting

SymptomCauseFix
Enemies always play perfectly or terriblyIntelligence pinned by the encounter's Min and Max, or by RSE.Difficulty.OverrideWiden the limits, set the override to -1
Changing the encounter's Intelligence does nothingAdaptive difficulty is on, and it ignores that fieldPin with Min and Max Intelligence, or turn Difficulty → Enabled off
The AI never gets harderFewer than Min Battles To Adjust battles, or the player sits inside the Dead ZonePlay more battles, or lower the dead zone; use RSE.Difficulty.Log 1
A custom style does not appear in the New Enemy wizardIt was not created with CreateRun Create in the AI Style wizard
Pick Best Action always failsThe pawn is not a unit of a running battle, or it is not its turnCheck with Is Actor In Battle and Is Actors Turn; put RSE: In Battle with Require Own Turn above it
Execute Ability fails in battleThe battle refused the command (not active, not enough AP or MP, target out of range)See the log category LogRSEAIInterop
Our AI and my tree both move the bossThe unit is not excluded from the framework AIReturn Own Brain from Get Action Battle Control before the battle starts
A Grid Tactics or ATB side does nothingThe controller has a pawn, or was not claimedUse a controller with no pawn and Claim Battle Side with the side's team tag
A tree gives no coverA combat role missing, or collision only on the canopyCheck the blocking volume in the Combat Role Configurator and set the role by hand
Roles did nothing on the gridThe grid was not baked againBake the Tactics zones again and check Bake Uses Combat Roles

14.9 See also