AngelScript
Most of Fall of an Empire's content - every trait, building, unit, diplomatic action, policy, and scripted event - is written in AngelScript. If you want to add a new building or a new character trait, this is the page you want.
What AngelScript is
AngelScript is a small, fast scripting language with syntax similar to C#. Fall of an Empire uses Hazelight's Unreal Engine AngelScript, which lets gameplay logic live in plain .as text files that load at runtime instead of being compiled into the executable. The upshot is that if you write a text file, put it in a folder, the game picks it up, with no engine, no compiler and no build step in the way.
If you've written C# or TypeScript you'll be comfortable immediately. Classes, inheritance, methods, if/for, arrays, etc.
For the AngelScript language itself - syntax, overrides, reflection macros and the type system - the Hazelight AngelScript documentation is the best reference.
Editor tooling: autocomplete for the whole game API
You can write .as files in any text editor, but VS Code with angel-lsp is the easiest way to use it. It gives you autocomplete, go-to-definition, signature help and diagnostics for AngelScript files.
Install these:
- Visual Studio Code (or an editor based on it such as Cursor)
- The AngelScript Language Server extension:
sashi0034.angel-lsp
The game includes the definitions file the extension needs:
as.predefined is a generated declaration of the C++ and UObject API exposed to scripts. It is there so the language server knows types a mod can call and where to find documentation for them.
Setup
Copy the shipped file into the root of your mod folder, next to mod.json:
Then open Mods/MyContentPack/ itself in VS Code. The workspace root matters: as.predefined needs to be at the root of the folder VS Code has open.
After that, open a .as file and start typing. If you write:
AFaction PlayerFaction = StrategyGameplay::PlayerFaction;
PlayerFaction.
VS Code should offer the faction methods and properties the game exposes to scripts.
Shared SDK setup
If you don't want to copy as.predefined into every mod, point the extension at the shipped file from .vscode/settings.json:
{
"angelScript.forceIncludePredefined": [
"C:/Games/FallOfAnEmpire/ModdingTools/AngelScriptSDK/as.predefined"
]
}
Change the path to wherever the game is installed. Use forward slashes in the JSON path, even on Windows.
The forceIncludePredefined setting makes the shipped definitions available globally in that VS Code workspace.
These two settings are also useful in a mod workspace:
{
"angelScript.implicitMutualInclusion": true,
"angelScript.files.angelScript": ["**/*.as"]
}
implicitMutualInclusion helps the language server see classes across your mod's script files without manual includes. files.angelScript tells it to process .as files anywhere under the folder you opened.
Auto-discovery: use a script root
Content is discovered automatically, but only inside the script roots for your mod. By default that means a folder called Script/ next to mod.json. You can also list different folders in mod.json with scripts.roots.
Once the file is under a script root, the game searches for every class that extends a known base type and makes it available. Write a class that extends UBuilding and there is a new building in the game; write one that extends UTrait and there is a new trait. You don't register those classes anywhere else.
The base types you'll extend most often:
| Base class | What it adds |
|---|---|
UTrait | A character trait with stat effects, inheritance, gain/loss rules |
UBuilding | A settlement building with costs, effects, and level progression |
UUnit | A military unit type with damage, cost, and culture restrictions |
UDiplomacyInteraction | A diplomatic action between factions |
UInteraction (and its spy/scheme subclasses) | Personal schemes, spy operations, political manoeuvres |
UScriptedEvent | A hand-authored event with branching options and effects |
UBridgeAction | An action your WebUI can call - see Hooking Into Game Systems |
Where mod scripts live
Inside your mod, scripts normally go under a Script/ folder. You can declare that in mod.json:
"scripts": { "roots": ["Script"] }
If your mod has a Script/ folder and you don't specify scripts at all, the game assumes Script anyway - but it's good practice to be explicit. You can list more than one root if you like to organise things.
Folder names under a script root are just organisation - the game finds .as files wherever they are inside that root. Mirroring the base game's layout (Buildings/, Traits/, Characters/Interactions/, and so on) keeps things readable.
A worked example: a building
The example below is a real base-game building, trimmed slightly. It shows the shape every building follows - a constructor that sets its data, and a few overrides that hook into the game when it's built and removed:
class UBakery : UBuilding
{
UBakery()
{
Name = Localization::GetText("Buildings", "Bakery_Name", "Bakery");
AssetKey = n"Bakery";
BuildTime = 60.0f;
Price = 200;
MaxLevel = 3;
Upkeep = 3;
BuildingType = EBuildingType::Other;
Category = EBuildingCategory::Economic;
ResourceCost.Add(n"Wood", 15.0f);
ResourceCost.Add(n"Stone", 20.0f);
DevelopedFrom = UFlourMill::StaticClass();
AvailableToCultureGroups.Add(n"Rephsian");
Description = Localization::GetText("Buildings", "Bakery_Description", "Large stone ovens...");
}
UFUNCTION(BlueprintOverride)
void OnAdd(APopulationCentre PopulationCentre)
{
PopulationCentre.Modifiers.FoodProduction.Set(n"Bakery", Name, 1.35f + Level * 0.30f);
}
UFUNCTION(BlueprintOverride)
void OnRemove(APopulationCentre PopulationCentre)
{
PopulationCentre.Modifiers.FoodProduction.Remove(n"Bakery");
}
}
A worked example: a trait
Traits lean on default - a shorthand for setting a property's default value on the class. Most of a trait is data, with optional logic for when it can be gained or lost:
UCLASS()
class UAdministrative : UTrait
{
default Key = n"Administrative";
UAdministrative()
{
ConflictingTraits.Add(UMilitant::StaticClass());
ConflictingTraits.Add(UImpetuous::StaticClass());
}
default TraitName = Localization::GetText("Traits", "Administrative_Name", "Administrative");
default Description = Localization::GetText("Traits", "Administrative_Description", "Excels at organisation...");
default Governance = 8.0f;
default Tactics = -5.0f;
default Loyalty = 1.0f;
default Heritability = 0.0f;
default Probability = 0.08f;
default bCanBeGained = true;
UFUNCTION(BlueprintOverride)
bool CanBeGained(UPerson Character) const
{
if (Character == nullptr) return false;
return Character.Stats.GetGovernance() > 12.0f
|| Character.HasTrait(UErudite::StaticClass());
}
UFUNCTION(BlueprintOverride)
bool CanBeLost(UPerson Character) const
{
return false; // once gained, kept for life
}
}
Want the "Paranoid" trait from the pitch - boosts cunning, tanks loyalty? It's this shape with default Cunning and default Loyalty set, plus whatever CanBeGained rule you like.
A worked example: an interaction
Diplomatic actions extend UDiplomacyInteraction. They declare their cost and difficulty as data, then override three things: whether they're available, their success chance, and what happens on completion.
class UAbandonFoederatiInteraction : UDiplomacyInteraction
{
default Key = n"AbandonFoederatiInteraction";
default InteractionName = Localization::GetText("Interactions", "AbandonFoederati_Name", "Abandon Foederati");
default Cost = 0;
default Difficulty = EInteractionDifficulty::VeryEasy;
UFUNCTION(BlueprintOverride)
TArray<FInteractionAvailabilityReason> BlueprintRequirements() const
{
TArray<FInteractionAvailabilityReason> Reasons;
AFaction SourceFaction = StrategyGameplay::PlayerFaction;
if (!SourceFaction.Vassals.Contains(Faction))
{
FInteractionAvailabilityReason Reason;
Reason.Reason = Localization::GetText("FactionInteractions", "AbandonFoederati_NotVassal", "Target is not your subject");
Reason.AvailabilityStatus = EInteractionAvailability::Hidden;
Reasons.Add(Reason);
}
return Reasons;
}
UFUNCTION(BlueprintOverride)
void OnInteractionComplete()
{
// apply the effects here
}
}
StrategyGameplay::PlayerFaction, Faction, Vassals - this is where scripting starts to reach into the running game. That (game state, factions, characters, settlements) is big enough to have its own page: Hooking Into Game Systems.
Events
Scripted events use UScriptedEvent and the same presentation and effect system as the base game's set-piece moments (famines, hordes, plagues, successions): flavour text, a set of options, and mechanical consequences per option, with the ability to chain follow-up events.
The game also has a dynamic event system where a local language model writes events from topics you provide, constrained so it can only ever reference characters, factions, and settlements that actually exist and apply effects from a defined set. Adding a topic to the events data table gives the model a new kind of scenario to generate. Both systems are covered alongside the base game's event content - start from the example mods, which include working scripted events to copy.
Next
- Reaching into the live game - factions, characters, save state, talking to your UI - Hooking Into Game Systems
- Turning off or replacing base-game content - Disabling & Overriding Content
- Data tables, icons, images, and localisation files - covered on those pages and in Testing & Publishing