Home

Scripted Events

Scripted events are narrative pop-ups with options that the player can respond to. They are weight-based (the system selects eligible events randomly, biased by weight), support context slots for dynamic text, and can form multi-step chains.


How Events Work#

  1. Discovery -- At startup, UGameDataSubsystem discovers all classes derived from UScriptedEvent.
  2. Eligibility check -- Periodically, UScriptedEventSubsystem iterates through all event classes, checks if each is eligible (game day, cooldown, required context, custom conditions), and builds a weighted pool.
  3. Selection -- One event is selected from the pool, weighted by BaseWeight.
  4. Context gathering -- The subsystem fills the event's RequiredSlots with matching entities from the game world (characters, factions, settlements, armies).
  5. Text substitution -- {SlotName} placeholders in titles, bodies, and option text are replaced with entity names.
  6. Display -- The event is shown to the player with its options.
  7. Resolution -- When the player picks an option, its effects are applied and any follow-up event is queued.

Event Properties#

PropertyTypeDefaultDescriptionEventKeyFName--Unique key for cooldown and fire-count tracking.ChainKeyFName--Shared key for all events in the same chain.bIsChainRootbooltrueWhether this event can be selected as a chain starter. Follow-up events set this to false.EventTitleFString--Title text. Supports {SlotName} substitution.EventBodyFString--Body text. Supports {SlotName} substitution.BaseWeightfloat100Selection weight. Higher = more likely to be chosen.MinGameDayint320Earliest game day the event can fire.MaxGameDayint320Latest game day. 0 = no limit.CooldownDaysint320Minimum days between firings. 0 = uses bOneTimeOnly.bOneTimeOnlyboolfalseIf true, fires at most once per campaign.RequiredSlotsTArray<FContextSlot>--Context slots the event needs (characters, factions, etc.). Set in constructor.RequiredConditionsTArray<EContextSlotType>--Situational conditions that must be true (e.g., AtWar, TreasuryLow). Set in constructor.ImageCategoriesTArray<FName>--Image category tags for artwork selection. Set in constructor.

Context Slots#

Context slots define what entities the event needs from the game world. Each slot has a name and a type. The subsystem attempts to fill each slot with a matching entity; if any required slot cannot be filled, the event is ineligible.

FContextSlot Structure

FieldTypeDescriptionSlotNameFNameUnique name within the event. Used for {SlotName} substitution and effect targeting.SlotTypeEContextSlotTypeWhat kind of entity to find.DependsOnFNameOptional. Name of another slot this depends on (e.g., a FactionLeader depends on a faction slot).

Setting Up Slots

Slots are added in the constructor:

angelscript
UMyEvent()
{
    FContextSlot Slot;
    Slot.SlotName = n"target_city";
    Slot.SlotType = EContextSlotType::PlayerSettlement;
    RequiredSlots.Add(Slot);

    // Dependent slot: governor of the settlement
    FContextSlot GovSlot;
    GovSlot.SlotName = n"governor";
    GovSlot.SlotType = EContextSlotType::Governor;
    GovSlot.DependsOn = n"target_city";
    RequiredSlots.Add(GovSlot);
}

Context Slot Type Reference

Character Slots

ValueDescriptionDependsOnHeirThe player's designated successor.--SpouseThe player's spouse.--PlayerChildA child of the player character.--PlayerFamilyA family member of the player.--CourtierA courtier from the player's court.--GovernorA governor of a player settlement.Settlement slotArmyCommanderA commander of a player army.--FactionLeaderThe leader of a referenced faction.Faction slotFriendA friend of the player character.--EnemyAn enemy of the player character.--PatronA patron of the player character.--ClientA client of the player character.--

Faction Slots

ValueDescriptionNeighborFactionA faction that shares a border with the player.EnemyFactionA faction currently at war with the player.VassalFactionA vassal state of the player.AlliedFactionA faction allied with the player.RivalFactionA faction with negative opinion, not at war.

Settlement Slots

ValueDescriptionDependsOnPlayerSettlementAny settlement owned by the player.--TroubledSettlementA settlement with high unrest.--SiegedSettlementA settlement currently under siege.--BorderSettlementA settlement near enemy territory.--WealthySettlementA high-population, prosperous settlement.--PortSettlementA port settlement.--GovernedSettlementThe settlement governed by a referenced governor.Governor slotOccupiedSettlementA settlement occupied by an enemy.--

Army Slots

ValueDescriptionDependsOnPlayerArmyAny army owned by the player.--LowMoraleArmyAn army with low morale.--BesiegingArmyAn army currently besieging a settlement.--DeployedArmyAn army deployed away from home territory.--EnemyArmyAn enemy army.--CommandedArmyThe army led by a referenced commander.Commander slot

Situational Conditions

These are used in RequiredConditions, not as slots. They check global game state.

ValueDescriptionAtWarPlayer is currently at war.AtPeacePlayer is at peace with all factions.TreasuryLowPlayer's treasury is low.TreasuryHighPlayer's treasury is high.FoodShortagePlayer is experiencing a food shortage.RecentBattleA battle occurred recently.RecentVictoryPlayer won a battle recently.RecentDefeatPlayer lost a battle recently.

Power Bloc Slots

ValueDescriptionDependsOnPlayerBlocA power bloc the player belongs to.--UnhappyBlocA power bloc unhappy with the player.--BlocLeaderThe leader of a power bloc.Bloc slotBlocMemberA member of a specific bloc.Bloc slot

Options#

Options are what the player chooses from when an event fires. Each option can have effects, trait requirements, and follow-up events.

FScriptedEventOption Structure

FieldTypeDefaultDescriptionTextFString--Button text. Supports {SlotName} substitution.TooltipFString--Hover tooltip. Supports {SlotName} substitution.EffectsTArray<FScriptedEffect>--Effects applied when this option is chosen.RequiredTraitsTArray<TSubclassOf<UTrait>>--Traits the player must have for this option to appear.FollowUpEventTSubclassOf<UScriptedEvent>--Event triggered after this option. null = chain ends.FollowUpDelayDaysint320Days to wait before showing the follow-up.

Building Options

Override BuildOptions() to define your event's choices:

angelscript
UFUNCTION(BlueprintOverride)
void BuildOptions(const FEventContext&in Context, TArray<FScriptedEventOption>&out OutOptions) const
{
    FScriptedEventOption Option;
    Option.Text = Localization::GetText("MyMod", "Option1_Text").ToString();
    Option.Tooltip = Localization::GetText("MyMod", "Option1_Tooltip").ToString();

    FScriptedEffect Effect;
    Effect.Type = EEffectType::Money;
    Effect.Value = 500;
    Effect.Description = Localization::GetText("MyMod", "Option1_GoldGain").ToString();
    Option.Effects.Add(Effect);

    OutOptions.Add(Option);
}

Effects#

Each option can apply multiple effects when chosen.

FScriptedEffect Structure

FieldTypeDefaultDescriptionTypeEEffectType--What kind of effect to apply.TargetSlotFName--Which context slot the effect targets (resolved to an entity at runtime).SecondarySlotFName--Secondary target (for relationship/opinion effects).Valuefloat0Numeric value (gold amount, stat change, opinion modifier, etc.).ParameterFString--String parameter (stat name, trait class name, status action, etc.).DescriptionFString--Display text for the effect. Supports {SlotName} substitution.

Effect Type Reference

TypeDescriptionKey FieldsMoneyAdd or remove gold.Value = amount (positive = gain, negative = loss).StatModify a character stat.TargetSlot = character, Parameter = stat name, Value = change.AddTraitAdd a trait to a character.TargetSlot = character, Parameter = trait class name.RemoveTraitRemove a trait from a character.TargetSlot = character, Parameter = trait class name.CharacterDeathKill a character.TargetSlot = character.RelationshipModify relationship between two characters.TargetSlot = character A, SecondarySlot = character B, Value = change.OpinionChange opinion between characters.TargetSlot = character, Value = opinion change.CharacterStatusApply a status effect.TargetSlot = character, Parameter = status action.FameModify a character's fame.Value = fame change.RecruitmentGenerate troops.Value = troop count.TreatyCreate or modify a treaty.Context-dependent.SettlementAffect settlement properties.TargetSlot = settlement, Value = change.ArmyAffect army properties.TargetSlot = army, Value = change.FoodModify food supply.Value = food change.RoleChange a character's court role.TargetSlot = character, Parameter = role name.SpawnHordeCreate a new horde/threat.Context-dependent.

Effect Examples

angelscript
// Give gold
FScriptedEffect GoldGain;
GoldGain.Type = EEffectType::Money;
GoldGain.Value = 1000;
GoldGain.Description = "Gain 1000 gold";

// Reduce opinion of a character
FScriptedEffect OpinionLoss;
OpinionLoss.Type = EEffectType::Opinion;
OpinionLoss.TargetSlot = n"rival_lord";
OpinionLoss.Value = -30;
OpinionLoss.Description = "{rival_lord} loses respect for you";

// Add a trait
FScriptedEffect AddTrait;
AddTrait.Type = EEffectType::AddTrait;
AddTrait.TargetSlot = n"courtier";
AddTrait.Parameter = "UBattleHardened";
AddTrait.Description = "{courtier} gains the Battle Hardened trait";

// Kill a character
FScriptedEffect Death;
Death.Type = EEffectType::CharacterDeath;
Death.TargetSlot = n"enemy_commander";
Death.Description = "{enemy_commander} dies";

// Modify fame
FScriptedEffect FameLoss;
FameLoss.Type = EEffectType::Fame;
FameLoss.Value = -150;
FameLoss.Description = "Your reputation suffers";

Text Substitution#

Any {SlotName} placeholder in EventTitle, EventBody, option Text, option Tooltip, and effect Description is replaced with the display name of the entity in that slot.

angelscript
default EventTitle = "Trouble in {target_city}";
default EventBody = "{governor} has been accused of corruption in {target_city}. The {unhappy_bloc} demands action.";

If slot target_city is filled with a settlement named "Augustum", the title becomes "Trouble in Augustum".


Event Chains#

Events can link together into multi-step narratives. Each option can specify a follow-up event that fires after a delay.

How Chains Work

  1. The root event fires and the player picks an option with a FollowUpEvent.
  2. The context (all filled slots) is preserved.
  3. After FollowUpDelayDays, the follow-up event fires with the same context.
  4. The follow-up's options can chain to further events, or end the chain (no FollowUpEvent).

Chain Rules

  • All events in a chain share the same ChainKey.
  • Only the root event has bIsChainRoot = true.
  • Follow-up events set bIsChainRoot = false so they are never selected independently.
  • Follow-ups do not need RequiredSlots or RequiredConditions -- they inherit context from the chain.
  • Follow-ups still need EventKey for cooldown/tracking.

Walkthrough: Building a Two-Event Chain

Step 1: Root Event

angelscript
class UBanditThreatRoot : UScriptedEvent
{
    default EventKey = n"BanditThreatRoot";
    default ChainKey = n"BanditThreatChain";
    default bIsChainRoot = true;
    default EventTitle = Localization::GetText("MyMod", "BanditThreat_Title").ToString();
    default EventBody = Localization::GetText("MyMod", "BanditThreat_Body").ToString();
    default BaseWeight = 80.0f;
    default MinGameDay = 30;
    default CooldownDays = 300;

    UBanditThreatRoot()
    {
        FContextSlot Settlement;
        Settlement.SlotName = n"target_town";
        Settlement.SlotType = EContextSlotType::BorderSettlement;
        RequiredSlots.Add(Settlement);

        FContextSlot Commander;
        Commander.SlotName = n"local_commander";
        Commander.SlotType = EContextSlotType::ArmyCommander;
        RequiredSlots.Add(Commander);

        ImageCategories.Add(n"bandit-raid");
    }

    UFUNCTION(BlueprintOverride)
    bool IsEligible(const FEventContext&in Context) const
    {
        return true;
    }

    UFUNCTION(BlueprintOverride)
    void BuildOptions(const FEventContext&in Context, TArray<FScriptedEventOption>&out OutOptions) const
    {
        // Option 1: Send the army (chains to resolution)
        FScriptedEventOption SendArmy;
        SendArmy.Text = Localization::GetText("MyMod", "BanditThreat_SendArmy").ToString();
        SendArmy.Tooltip = Localization::GetText("MyMod", "BanditThreat_SendArmy_Tip").ToString();
        SendArmy.FollowUpEvent = UBanditThreatResolution::StaticClass();
        SendArmy.FollowUpDelayDays = 14;

        FScriptedEffect ArmyCost;
        ArmyCost.Type = EEffectType::Money;
        ArmyCost.Value = -500;
        ArmyCost.Description = Localization::GetText("MyMod", "BanditThreat_ArmyCost").ToString();
        SendArmy.Effects.Add(ArmyCost);

        OutOptions.Add(SendArmy);

        // Option 2: Pay tribute (no follow-up, chain ends)
        FScriptedEventOption PayTribute;
        PayTribute.Text = Localization::GetText("MyMod", "BanditThreat_PayTribute").ToString();
        PayTribute.Tooltip = Localization::GetText("MyMod", "BanditThreat_PayTribute_Tip").ToString();

        FScriptedEffect TributeCost;
        TributeCost.Type = EEffectType::Money;
        TributeCost.Value = -1000;
        TributeCost.Description = Localization::GetText("MyMod", "BanditThreat_TributeCost").ToString();
        PayTribute.Effects.Add(TributeCost);

        FScriptedEffect FameLoss;
        FameLoss.Type = EEffectType::Fame;
        FameLoss.Value = -100;
        FameLoss.Description = Localization::GetText("MyMod", "BanditThreat_FameLoss").ToString();
        PayTribute.Effects.Add(FameLoss);

        OutOptions.Add(PayTribute);

        // Option 3: Ignore (no follow-up, chain ends)
        FScriptedEventOption Ignore;
        Ignore.Text = Localization::GetText("MyMod", "BanditThreat_Ignore").ToString();
        Ignore.Tooltip = Localization::GetText("MyMod", "BanditThreat_Ignore_Tip").ToString();
        OutOptions.Add(Ignore);
    }
}

Step 2: Follow-Up Event

angelscript
class UBanditThreatResolution : UScriptedEvent
{
    default EventKey = n"BanditThreatResolution";
    default ChainKey = n"BanditThreatChain";     // Same chain key
    default bIsChainRoot = false;                  // Not independently selectable
    default EventTitle = Localization::GetText("MyMod", "BanditResolution_Title").ToString();
    default EventBody = Localization::GetText("MyMod", "BanditResolution_Body").ToString();

    UBanditThreatResolution()
    {
        ImageCategories.Add(n"military-camp");
    }

    UFUNCTION(BlueprintOverride)
    void BuildOptions(const FEventContext&in Context, TArray<FScriptedEventOption>&out OutOptions) const
    {
        // Option 1: Victory
        FScriptedEventOption Victory;
        Victory.Text = Localization::GetText("MyMod", "BanditResolution_Victory").ToString();
        Victory.Tooltip = Localization::GetText("MyMod", "BanditResolution_Victory_Tip").ToString();

        FScriptedEffect FameGain;
        FameGain.Type = EEffectType::Fame;
        FameGain.Value = 200;
        FameGain.Description = Localization::GetText("MyMod", "BanditResolution_FameGain").ToString();
        Victory.Effects.Add(FameGain);

        OutOptions.Add(Victory);
    }
}

Overridable Methods#

MethodWhen CalledPurposebool IsEligible(const FEventContext&in Context) constDuring eligibility checkCustom eligibility logic beyond slots and conditions. Return true to allow.float GetWeight(const FEventContext&in Context) constDuring weight calculationAdjust the event's selection weight based on context. Return BaseWeight by default.void BuildOptions(const FEventContext&in Context, TArray<FScriptedEventOption>&out OutOptions) constWhen displaying the eventBuild the list of options for the player.

Image Categories#

The ImageCategories array controls which event artwork is displayed. The system selects an image tagged with one of the specified categories from the EventImages DataTable.

angelscript
UMyEvent()
{
    ImageCategories.Add(n"throne-room");
    ImageCategories.Add(n"diplomacy");
}

Multiple categories increase the chance of finding a matching image. If no match is found, a generic image is used.


Tips#

  • Keep BaseWeight values in a reasonable range (50--200). Very high weights dominate the selection pool.
  • Use MinGameDay to prevent early-game events from firing before the player has established themselves (30--60 is typical).
  • Set generous CooldownDays to prevent events from feeling repetitive (180--400 for non-chain events).
  • bOneTimeOnly = true is best for major story events or tutorial events.
  • Follow-up delays of 7--30 days feel natural. Very short delays (1--3 days) feel like the same event, and very long delays (60+) may lose narrative momentum.
  • The IsEligible() override is useful for checking complex conditions that cannot be expressed through slots alone (e.g., checking if a specific building exists in a settlement).
  • For EventTitle and EventBody, use .ToString() when assigning from Localization::GetText() since these are FString properties.
  • Effect descriptions should be concise. They appear in the option tooltip alongside the effect icon.

Next Steps#