Structure === Note: May not be exhaustive. Event IDs are namespaced. In each events file, define a namespace once at the top: namespace = my_events Then define events as: my_events.1001 = { ... } my_events.1001 = { type = character_event/letter_event/court_event/activity_event # Optional, defaults to character_event scope = scope_type # Overrides the events root scope. Optional, defaults to character scope. Use scope = none for no root scope, scope = artifact for events centered around artifacts, etc. # Optional custom event window name. # For character/letter events this is the popup window in gui/event_windows. # For activity events this is used by activity event inserts. window = window_name # anonymous_letter_event - letter_event, but without sender portrait and sigil widget # big_event_window - used for task_contracts, bookmark events, decision outcomes, story cycles, black plague, etc. # character_event - default # duel_event - used in single combat events # fullscreen_event - use splash screen queue # letter_event - used for responses to character_interactions, between characters that are not in the same location # scheme_conclusion_window - big_event_window, however we always use one of the sub-types: # scheme_failed_event - scheme_conclusion_window, but with failure header # scheme_preparations_event - scheme_conclusion_window, but with scheme_preparation widget # scheme_successful_event - scheme_conclusion_window, but with successful header # scheme_conclusion_event_no_header # scheme_conclusion_window, but without header #visit_settlement_window - big_event_window, used by laamp visit settlement decision # Basic event text/effects (commonly used) title = my_event_title # Dynamic Description syntax, see bottom of document desc = my_event_desc # Dynamic Description syntax, see bottom of document trigger = { ... } immediate = { ... } after = { ... } hidden = yes/no # If yes, no event window is shown major = yes/no major_trigger = { ... } # Non-character scoped events generally need to be hidden or major. # If you have a cooldown, the recipient root gets a saved variable with that duration. # The variable name is based on the event ID. # Trigger legality checks include cooldown. cooldown = { days/weeks/months/years = script value } # DLC or Mod that this Event belongs to, shown in Event Window if set. content_source = X # Specify a character portrait to appear in the event on the specified position. left_portrait = X right_portrait = X center_portrait = X # not used in all event types lower_left_portrait = X lower_center_portrait = X lower_right_portrait = X sender = X # required for letter events # X can be one of: X = event target X = { character = event target trigger = ... # optional, controls visibility for this portrait in the scope of the portrait character animation = animation name # optional default animation, used if no triggered animations pass their trigger. See animations.txt for all possible animations, in-game: toggle event tools in the event or open portrait editor through the console. scripted_animation = key_of_scripted_animation # optional alternative to animation # First triggered animation whose trigger passes is used. triggered_animation = { trigger = ... animation = animation name # Or use scripted_animation instead of animation scripted_animation = key_of_scripted_animation camera = camera_name # optional, overrides portrait camera when chosen } triggered_animation = ... # First triggered outfit whose trigger passes is used. triggered_outfit = { trigger = ... outfit_tags = ... remove_default_outfit = ... } triggered_outfit = ... # Optional portrait behavior toggles camera = camera_key override_imprisonment_visuals = yes/no animate_if_dead = yes/no outfit_tags = { tag1 tag2 } # Specifies outfit tags for this portrait in ascending priority (i.e. tag2 will "override" tag1 here if anything with tag2 is found in a specific portrait modifier category) remove_default_outfit = yes/no # If set to yes, portrait modifier categories in which nothing matches any of the event tags will be disabled completely (no by default) hide_info = yes/no # If set to yes, only the portrait will be shown, with no identifiable elements (no CoA, tooltips, clicks...) (no by default) } # Specify an artifact to appear in the event on the specified position artifact = { target = event target position = lower_left_portrait/lower_center_portrait/lower_right_portrait # Cannot share the same position as a portrait trigger = ... # Optional, as for portraits } # Letter events should define an opening text opening = my_letter_opening # Localization key or dynamic desc block # Court events may define court scene behavior court_scene = { button_position_character = scope:a_character court_owner = scope:a_character court_event_force_open = yes/no show_timeout_info = yes/no should_pause_time = yes/no roles = { scope:a_character = { role = some_court_scene_role # or group = some_court_scene_group animation = some_animation scripted_animation = some_scripted_animation } } } # Runs if a queued/instant event fails trigger checks. # Events selected from on_actions are filtered by validity before queuing, so this is typically not run for that path. on_trigger_fail = { some effect } # Specify custom widgets to embed in the event. See Custom Widgets below. widgets = { widget = { # Scope: event scope after immediate effect. Default: always = yes is_shown = {} # Widget file at /.gui gui = "" # Parent container widget name in the event window container = "" # Controller syntax: # Simple form: controller = # Structured form (used by some controllers, e.g. text): # controller = { # type = # data = { ... } # } # Optional scope setup effect for controller expectations setup_scope = {} } } widget = { ... } # alternative syntax for one widget option = { # An option the player/AI can pick # Localization key or dynamic name block name = X # Dynamic Description syntax, see bottom of document # Effects run when this option is picked (inline, no label) X.. # Trigger required for option to be valid trigger = {} # If the option is invalid, but this trigger is valid, then the option will be shown (but disabled). # This behavior is also influenced by the EVENT_OPTIONS_SHOWN_HIDE_UNAVAILABLE or SCHEME_PREPARATION_OPTIONS_SHOWN_HIDE_UNAVAILABLE defines depending on event type. show_as_unavailable = {} # Highlights the event portrait of this character while this option is hovered. This is in addition to the automatic highlighting when hovering an event option that has an effect that affects portrait characters. highlight_portrait = scope:a_character reason = # Special reason for why this option is unlocked, can be any arbitrary string, is be checked in the UI to show special by reason skill = diplomacy/martial/stewardship/intrigue/learning/prowess # Marks this option as skill-relevant in unlock reason UI (if shown); you still have to specify the skill and level in the trigger to unlock it trait = some_trait # Marks this option as trait-relevant in unlock reason UI (if shown); you still have to specify the trait in the trigger to unlock it # Misc option behavior show_unlock_reason = yes/no # Controls whether unlock reason UI is shown for this option is_cancel_option = yes/no # Marks this option as a cancel/back-out style option (used by some widgets/controllers) clicksound = "sound_event" # Sound to play when selecting this option fallback = yes/no # If no regular options are valid, fallback options are considered exclusive = yes/no # If any exclusive option is valid, non-exclusive options are ignored # Parameters to impact the way ai-characters pick options to resolve their events. # We have 2 mutually exclusive parameters; ai_chance, and ai_will_select where the only difference is the syntax for calculating the value. # In practice if both are present, ai_will_select is used. ai_chance = { # See common/scripted_modifiers/_scripted_modifiers.info for more details base = 10 modifier = { add = 5 } modifier = { factor = 0.5 } } ai_will_select = { # See common/script_values/_script_values.info for more details base = 10 if = { limit = { } add = 5 } else_if = { limit = { } multiply = 0.5 } } } theme = "" # Theme to use in the event. For a list, check: 00_event_themes.txt override_background = { # A background that can be shown when the event pops up. This overrides the theme one. In case that there are multiples the first one that fits the trigger will be the one selected. In case none fits the ones in the theme will be checked after. trigger = {} # Receives the event scope to check if it's valid. reference = "" # Path to the texture } override_transition = { # A transition that can be shown when the event pops up, before the event options and backgrounds. This overrides the theme one. In case that there are multiples the first one that fits the trigger will be the one selected. In case none fits the ones in the theme will be checked after. trigger = {} # Receives the event scope to check if it's valid. reference = "" # Path to the texture } override_effect_2d = { # A 2d effect that can be put on top of the background. This overrides the theme one. In case that there are multiples the first one that fits the trigger will be the one selected. In case none fits the ones in the theme will be checked after. trigger = {} # Receives the event scope to check if it's valid. reference = "" # key to the effect } override_icon = { # An icon that can be shown when the event pops up. This overrides the theme one. In case that there are multiples the first one that fits the trigger will be the one selected. In case none fits the ones in the theme will be checked after. trigger = {} # Receives the event scope to check if it's valid. reference = "" # Path to the texture } override_header_background = { # The header asset located behind the event icon. This overrides the header asset defined by the theme. If there are multiples defined here, the first one that passes its trigger will be selected. If none are valid, then the theme's header asset will be used trigger = {} reference = "" } override_sound = { # A sound that can be played when the event pops up. This overrides the theme one. In case that there are multiples the first one that fits the trigger will be the one selected. In case none fits the ones in the theme will be checked after. trigger = {} # Receives the event scope to check if it's valid. reference = "" # Reference of the sound } orphan = yes # The game will not log an error about this event being unreferenced. Useful for debug events } === Custom Widgets === Custom widgets can be embedded into events. GUI files must be placed at the event_window_widgets path (see paths.settings). The name of the file must match the widget name. Some widgets that modify the game require a custom controller. This should be documented in the widget's GUI file. The data context type available in the GUI depends on the controller type. Some controllers require special scope setup, which should be documented under Notes below. Use the setup_scope effect for that. Available controllers: Controller Type | Data Context Name | Notes ------------------------+----------------------------------------+------------------------------------------------------------------------------------------------------------- default | EventWindowWidget | Default controller, no special behavior name_character | EventWindowWidgetNameCharacter | Changes a character's name. Scope must have the name_character_target saved scope. text | EventWindowWidgetEnterText | Saves some text onto the character. May use controller = { type = text data = { ... } } and setup_scope for text_target. event_chain_progress | EventWindowWidgetChainProgress | Displays progress through an event chain, needs event_chain_length and event_chain_progress scope values set struggle_info | EventWindowCustomWidgetStruggleInfo | Displays information for the struggle, needs "start" scope value set situation_info | EventWindowCustomWidgetSituationInfo | Displays information for the situation === Dynamic Description Appendix === Dynamic descriptions are supported by event `title`, `desc`, `opening`, and option `name` (with special notes for option names below). They resolve localization keys at runtime using current event scopes. High-level behavior: - A plain localization key is valid: - `desc = my_event.desc` - A block is also valid: - `desc = { ... }` - In blocks, entries are evaluated in script order. - Each selected piece is appended to the final text with automatic spacing between pieces. - Missing localization keys are logged as errors. Core block members (CDynamicDescription): 1) `desc` - Appends text to output. - Accepts either a key or another nested dynamic description block. Example: desc = { desc = my_event.intro desc = my_event.outro } 2) `triggered_desc` - Conditional description node. - Body may only contain: - `trigger = { ... }` (optional; if omitted, always valid) - `desc = ` or `desc = { ... }` - If trigger passes, its `desc` content is appended. Example: triggered_desc = { trigger = { has_trait = brave } desc = my_event.brave_line } 3) `first_valid` - Checks child entries in order. - Uses only the first child whose validity is true. - Typical fallback pattern is placing a plain `desc = some_fallback_key` last. Example: first_valid = { triggered_desc = { trigger = { has_trait = brave } desc = my_event.brave } triggered_desc = { trigger = { has_trait = craven } desc = my_event.craven } desc = my_event.fallback } 4) `random_valid` - Collects all valid children, then picks random entries from that set. - Default picks: `count = 1` - Optional: `count = N` picks up to N unique valid children (no replacement). - If fewer than N are valid, all valid children are used. - Chosen entries are appended by the random order. Example: random_valid = { count = 2 desc = my_event.random_a triggered_desc = { trigger = { has_trait = patient } desc = my_event.random_b } desc = my_event.random_c } 5) `switch` - Value-based branching for description selection. - Structure: - `trigger = ` - ` = ` - `fallback = ` (optional) - First matching case is used. - If no case matches and no `fallback` exists, nothing is appended. - Case values are written as keys/tokens that match what the switch trigger returns. Example: switch = { trigger = scope:rite_memory.var:rites_of_passage_type flag:dueling_rite_memory = { desc = bp2_yearly.7029.desc_duel } flag:scarification_rite_memory = { desc = bp2_yearly.7029.desc_scarification } fallback = { desc = bp2_yearly.7029.desc } } Composition and nesting: - Nodes can be nested arbitrarily (`desc` in `desc`, `first_valid` inside `random_valid`, etc.). Option name specifics: - Option names support two forms: - `name = my_option_text_key` - `name = { text = trigger = { ... } }` - `trigger` inside `name = { ... }` gates whether that name candidate is available. - Multiple `name = { ... }` blocks are allowed per option. - If more than one candidate is valid, one valid candidate is chosen randomly. - If none are valid, first defined candidate is used as fallback. - Important limitation: name candidates are separate sibling `name` entries; you cannot nest multiple `name` nodes inside one `name` block. Known-good vanilla patterns to copy: - Multi-chunk nested assembly with `random_valid`, `first_valid`, and `triggered_desc`: - `game/events/board_game_events.txt` (e.g. event `board_games.0041`) - `game/events/relations_events/adultery_events.txt` (e.g. event `adultery.4001`) - Standard fallback chain in `first_valid`: - `game/events/relations_events/adultery_events.txt` (e.g. event `adultery.1006`) - Letter `opening` using dynamic desc block: - `game/events/interaction_events/marriage_interaction_events.txt` (e.g. event `marriage_interaction.0001`) - Dynamic description `switch`: - `game/events/dlc/bp2/bp2_yearly_7.txt` (e.g. event `bp2_yearly.7029`) Practical authoring checklist: - Always include a safe fallback path (`desc` fallback in `first_valid`, or `fallback` in `switch`). - Keep localization keys flat and explicit; nesting controls selection, loc does the prose. - When building long descriptions, split into semantic chunks and compose with nested blocks. - Prefer `first_valid` for deterministic branching and `random_valid` for variation. - For option names, prefer multiple gated `name` entries over overly complex single-entry nesting. COMMON GOTCHA WHEN BUILDING DYNAMIC DESCRIPTIONS, FOR INTERNAL DESIGNERS: If you're building a complex loc string that includes dialogue, it's common to end up with keys that contains an odd number of citation marks. In these cases you may need to escape the odd citation marks using double-backslash to pass the sanity checking git hook before you commit your work. E.g.: my_cool_event_desc.intro: "The guy says, \\"Hey " my_cool_event_desc.intro.friend: "friend" my_cool_event_desc.intro.buddy: "buddy" my_cool_event_desc.intro.idiot: "idiot" my_cool_event_desc.intro.outro: ", watch where you're going!\\" I pay him no mind and continue on my way."