class_name EventDefinition extends RefCounted const EventConditionType = preload("res://scripts/events/event_condition.gd") const EventActionType = preload("res://scripts/events/event_action.gd") const EventChoiceType = preload("res://scripts/events/event_choice.gd") enum TransactionPolicy { ROLLBACK_ALL, SKIP_FAILED, BRANCH_FAILURE, } enum OnceScope { NONE, PER_DAY, PER_ACTOR_PAIR, PER_SAVE, } enum MissingActorPolicy { CANCEL, REPLACE_WITH_ROLE, CONTINUE_WITHOUT, BRANCH_FAILURE, } enum ContentRating { GENERAL, MATURE, RESTRICTED, } enum FilterBehavior { NONE, FADE, REPLACE, DISABLE, } var event_id: StringName = &"" var category: StringName = &"FLAVOR" var priority := 0 var conditions: Array[EventCondition] = [] var participant_roles: Array[StringName] = [] var location_rule: Dictionary = {} var start_window: Array[int] = [] var expires_after := 0 var foreground := true var interrupt_policy: StringName = &"NORMAL" var choices: Array[EventChoice] = [] var automatic_actions: Array[EventAction] = [] var fallback_actions: Array[EventAction] = [] var failure_actions: Array[EventAction] = [] var replacement_event_id: StringName = &"" var once_scope: OnceScope = OnceScope.NONE var transaction_policy: TransactionPolicy = TransactionPolicy.ROLLBACK_ALL var missing_actor_policy: MissingActorPolicy = MissingActorPolicy.CANCEL var content_rating: ContentRating = ContentRating.GENERAL var content_tags: Array[StringName] = [] var filter_behavior: FilterBehavior = FilterBehavior.NONE func configure(p_event_id: StringName, options: Dictionary = {}) -> EventDefinition: event_id = p_event_id category = StringName(_value(options, &"category", &"FLAVOR")) priority = int(_value(options, &"priority", 0)) conditions = _conditions(_value(options, &"conditions", [])) participant_roles = _sorted_unique_names(_value(options, &"participants", [])) location_rule = _dictionary(_value(options, &"location_rule", {})).duplicate(true) start_window = _integer_array(_value(options, &"start_window", [])) expires_after = maxi(int(_value(options, &"expires_after", 0)), 0) foreground = bool(_value(options, &"foreground", true)) interrupt_policy = StringName(_value(options, &"interrupt_policy", &"NORMAL")) choices = _choices(_value(options, &"choices", [])) automatic_actions = _actions(_value(options, &"automatic_actions", [])) fallback_actions = _actions(_value(options, &"fallback_actions", [])) failure_actions = _actions(_value(options, &"failure_actions", [])) replacement_event_id = StringName(_value(options, &"replacement_event_id", &"")) once_scope = _enum_value(OnceScope.keys(), _value(options, &"once_scope", OnceScope.NONE), OnceScope.NONE) as OnceScope transaction_policy = _enum_value(TransactionPolicy.keys(), _value(options, &"transaction_policy", TransactionPolicy.ROLLBACK_ALL), TransactionPolicy.ROLLBACK_ALL) as TransactionPolicy missing_actor_policy = _enum_value(MissingActorPolicy.keys(), _value(options, &"missing_actor_policy", MissingActorPolicy.CANCEL), MissingActorPolicy.CANCEL) as MissingActorPolicy content_rating = _enum_value(ContentRating.keys(), _value(options, &"content_rating", ContentRating.GENERAL), ContentRating.GENERAL) as ContentRating content_tags = _sorted_unique_names(_value(options, &"content_tags", [])) filter_behavior = _enum_value(FilterBehavior.keys(), _value(options, &"filter_behavior", FilterBehavior.NONE), FilterBehavior.NONE) as FilterBehavior return self func is_valid() -> bool: if event_id.is_empty() or category.is_empty(): return false if filter_behavior == FilterBehavior.REPLACE and replacement_event_id.is_empty(): return false if replacement_event_id == event_id: return false if start_window.size() not in [0, 2]: return false for condition: EventCondition in conditions: if condition == null or not condition.is_valid(): return false var choice_ids: Dictionary = {} for choice: EventChoice in choices: if choice == null or not choice.is_valid() or choice_ids.has(choice.choice_id): return false choice_ids[choice.choice_id] = true for action_list: Array in [automatic_actions, fallback_actions, failure_actions]: for action: EventAction in action_list: if action == null or not action.is_valid(): return false return true func get_choice(choice_id: StringName) -> EventChoice: for choice: EventChoice in choices: if choice.choice_id == choice_id: return choice return null func conditions_match(context: Dictionary, callbacks: Dictionary = {}) -> bool: for condition: EventCondition in conditions: if not condition.evaluate(context, callbacks): return false return true func create_snapshot() -> Dictionary: return { &"event_id": String(event_id), &"category": String(category), &"priority": priority, &"conditions": _snapshots(conditions), &"participants": _names_to_strings(participant_roles), &"location_rule": location_rule.duplicate(true), &"start_window": start_window.duplicate(), &"expires_after": expires_after, &"foreground": foreground, &"interrupt_policy": String(interrupt_policy), &"choices": _snapshots(choices), &"automatic_actions": _snapshots(automatic_actions), &"fallback_actions": _snapshots(fallback_actions), &"failure_actions": _snapshots(failure_actions), &"replacement_event_id": String(replacement_event_id), &"once_scope": OnceScope.keys()[once_scope], &"transaction_policy": TransactionPolicy.keys()[transaction_policy], &"missing_actor_policy": MissingActorPolicy.keys()[missing_actor_policy], &"content_rating": ContentRating.keys()[content_rating], &"content_tags": _names_to_strings(content_tags), &"filter_behavior": FilterBehavior.keys()[filter_behavior], } static func from_variant(raw_value: Variant) -> EventDefinition: if raw_value is EventDefinition: return raw_value if not raw_value is Dictionary: return null # Serialized definitions are authoritative data, not a user-facing settings # form. Unknown enum names and out-of-range integer values must fail closed # instead of silently selecting a less restrictive fallback. var enum_fields := [ [&"once_scope", OnceScope.keys()], [&"transaction_policy", TransactionPolicy.keys()], [&"missing_actor_policy", MissingActorPolicy.keys()], [&"content_rating", ContentRating.keys()], [&"filter_behavior", FilterBehavior.keys()], ] for field: Array in enum_fields: var key: StringName = field[0] if raw_value.has(key) or raw_value.has(String(key)): if _strict_enum_value(field[1], _value(raw_value, key, null)) < 0: return null return EventDefinition.new().configure( StringName(_value(raw_value, &"event_id", &"")), raw_value ) static func _conditions(raw_value: Variant) -> Array[EventCondition]: var result: Array[EventCondition] = [] if raw_value is Array: for raw_entry: Variant in raw_value: var entry := EventConditionType.from_variant(raw_entry) if entry != null: result.append(entry) return result static func _choices(raw_value: Variant) -> Array[EventChoice]: var result: Array[EventChoice] = [] if raw_value is Array: for raw_entry: Variant in raw_value: var entry := EventChoiceType.from_variant(raw_entry) if entry != null: result.append(entry) return result static func _actions(raw_value: Variant) -> Array[EventAction]: var result: Array[EventAction] = [] if raw_value is Array: for raw_entry: Variant in raw_value: var entry := EventActionType.from_variant(raw_entry) if entry != null: result.append(entry) return result static func _snapshots(values: Array) -> Array[Dictionary]: var result: Array[Dictionary] = [] for value: Variant in values: result.append(value.create_snapshot()) return result static func _sorted_unique_names(raw_value: Variant) -> Array[StringName]: var result: Array[StringName] = [] if raw_value is Array: for raw_entry: Variant in raw_value: var entry := StringName(raw_entry) if not entry.is_empty() and not result.has(entry): result.append(entry) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result static func _integer_array(raw_value: Variant) -> Array[int]: var result: Array[int] = [] if raw_value is Array: for raw_entry: Variant in raw_value: result.append(int(raw_entry)) return result static func _names_to_strings(values: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in values: result.append(String(value)) return result static func _enum_value(keys: Array, raw_value: Variant, fallback: int) -> int: if raw_value is int: return clampi(int(raw_value), 0, keys.size() - 1) var index := keys.find(String(raw_value).to_upper()) return index if index >= 0 else fallback static func _strict_enum_value(keys: Array, raw_value: Variant) -> int: if raw_value is int: var numeric := int(raw_value) return numeric if numeric >= 0 and numeric < keys.size() else -1 if raw_value is String or raw_value is StringName: return keys.find(String(raw_value).to_upper()) return -1 static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: if source.has(key): return source[key] return source.get(String(key), fallback) static func _dictionary(value: Variant) -> Dictionary: return value if value is Dictionary else {}