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:

  1. Visual Studio Code (or an editor based on it such as Cursor)
  2. The AngelScript Language Server extension: sashi0034.angel-lsp

The game includes the definitions file the extension needs:

Files
FallOfAnEmpire/
└── ModdingTools/
└── AngelScriptSDK/
└── as.predefined

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:

Files
Mods/
└── MyContentPack/
├── mod.json
├── as.predefined
└── Script/
└── Buildings/
└── SicilianTunaWorks.as

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:

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

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:

JSON
{
  "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 classWhat it adds
UTraitA character trait with stat effects, inheritance, gain/loss rules
UBuildingA settlement building with costs, effects, and level progression
UUnitA military unit type with damage, cost, and culture restrictions
UDiplomacyInteractionA diplomatic action between factions
UInteraction (and its spy/scheme subclasses)Personal schemes, spy operations, political manoeuvres
UScriptedEventA hand-authored event with branching options and effects
UBridgeActionAn 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:

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.

Files
Mods/
└── MyContentPack/
├── mod.json
└── Script/
├── Buildings/
│ └── SicilianTunaWorks.as
└── Traits/
└── Ambitious.as

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:

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

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

AngelScript
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