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#
- Discovery -- At startup,
UGameDataSubsystemdiscovers all classes derived fromUScriptedEvent. - Eligibility check -- Periodically,
UScriptedEventSubsystemiterates through all event classes, checks if each is eligible (game day, cooldown, required context, custom conditions), and builds a weighted pool. - Selection -- One event is selected from the pool, weighted by
BaseWeight. - Context gathering -- The subsystem fills the event's
RequiredSlotswith matching entities from the game world (characters, factions, settlements, armies). - Text substitution --
{SlotName}placeholders in titles, bodies, and option text are replaced with entity names. - Display -- The event is shown to the player with its options.
- Resolution -- When the player picks an option, its effects are applied and any follow-up event is queued.
Event Properties#
EventKeyFNameChainKeyFNamebIsChainRootboolfalse.EventTitleFString{SlotName} substitution.EventBodyFString{SlotName} substitution.BaseWeightfloatMinGameDayint32MaxGameDayint320 = no limit.CooldownDaysint320 = uses bOneTimeOnly.bOneTimeOnlyboolRequiredSlotsTArray<FContextSlot>RequiredConditionsTArray<EContextSlotType>AtWar, TreasuryLow). Set in constructor.ImageCategoriesTArray<FName>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
SlotNameFName{SlotName} substitution and effect targeting.SlotTypeEContextSlotTypeDependsOnFNameFactionLeader depends on a faction slot).Setting Up Slots
Slots are added in the constructor:
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
HeirSpousePlayerChildPlayerFamilyCourtierGovernorArmyCommanderFactionLeaderFriendEnemyPatronClientFaction Slots
NeighborFactionEnemyFactionVassalFactionAlliedFactionRivalFactionSettlement Slots
PlayerSettlementTroubledSettlementSiegedSettlementBorderSettlementWealthySettlementPortSettlementGovernedSettlementOccupiedSettlementArmy Slots
PlayerArmyLowMoraleArmyBesiegingArmyDeployedArmyEnemyArmyCommandedArmySituational Conditions
These are used in RequiredConditions, not as slots. They check global game state.
AtWarAtPeaceTreasuryLowTreasuryHighFoodShortageRecentBattleRecentVictoryRecentDefeatPower Bloc Slots
PlayerBlocUnhappyBlocBlocLeaderBlocMemberOptions#
Options are what the player chooses from when an event fires. Each option can have effects, trait requirements, and follow-up events.
FScriptedEventOption Structure
TextFString{SlotName} substitution.TooltipFString{SlotName} substitution.EffectsTArray<FScriptedEffect>RequiredTraitsTArray<TSubclassOf<UTrait>>FollowUpEventTSubclassOf<UScriptedEvent>null = chain ends.FollowUpDelayDaysint32Building Options
Override BuildOptions() to define your event's choices:
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
TypeEEffectTypeTargetSlotFNameSecondarySlotFNameValuefloatParameterFStringDescriptionFString{SlotName} substitution.Effect Type Reference
MoneyValue = amount (positive = gain, negative = loss).StatTargetSlot = character, Parameter = stat name, Value = change.AddTraitTargetSlot = character, Parameter = trait class name.RemoveTraitTargetSlot = character, Parameter = trait class name.CharacterDeathTargetSlot = character.RelationshipTargetSlot = character A, SecondarySlot = character B, Value = change.OpinionTargetSlot = character, Value = opinion change.CharacterStatusTargetSlot = character, Parameter = status action.FameValue = fame change.RecruitmentValue = troop count.TreatySettlementTargetSlot = settlement, Value = change.ArmyTargetSlot = army, Value = change.FoodValue = food change.RoleTargetSlot = character, Parameter = role name.SpawnHordeEffect Examples
// 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.
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
- The root event fires and the player picks an option with a
FollowUpEvent. - The context (all filled slots) is preserved.
- After
FollowUpDelayDays, the follow-up event fires with the same context. - 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 = falseso they are never selected independently. - Follow-ups do not need
RequiredSlotsorRequiredConditions-- they inherit context from the chain. - Follow-ups still need
EventKeyfor cooldown/tracking.
Walkthrough: Building a Two-Event Chain
Step 1: Root Event
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
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#
bool IsEligible(const FEventContext&in Context) consttrue to allow.float GetWeight(const FEventContext&in Context) constBaseWeight by default.void BuildOptions(const FEventContext&in Context, TArray<FScriptedEventOption>&out OutOptions) constImage 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.
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
BaseWeightvalues in a reasonable range (50--200). Very high weights dominate the selection pool. - Use
MinGameDayto prevent early-game events from firing before the player has established themselves (30--60 is typical). - Set generous
CooldownDaysto prevent events from feeling repetitive (180--400 for non-chain events). bOneTimeOnly = trueis 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
EventTitleandEventBody, use.ToString()when assigning fromLocalization::GetText()since these areFStringproperties. - Effect descriptions should be concise. They appear in the option tooltip alongside the effect icon.
Next Steps#
- Content Types -- Traits, buildings, units, and other content
- Interactions -- Player and AI actions
- Assets & Packaging -- Icons, localisation, and distribution