def _add_traits(cls, sim_info):
     trait_tracker = sim_info.trait_tracker
     for trait in tuple(trait_tracker.personality_traits):
         trait_tracker.remove_trait(trait)
     for trait in cls._traits.explicit:
         trait_tracker.add_trait(trait)
     if cls._traits.num_random:
         num_to_add = cls._traits.num_random.random_int()
         if num_to_add > 0:
             trait_manager = services.trait_manager()
             available_trait_types = set([
                 trait for trait in trait_manager.types.values()
                 if trait.is_personality_trait
             ])
             available_trait_types = list(available_trait_types -
                                          cls._traits.blacklist -
                                          set(cls._traits.explicit))
             while True:
                 while num_to_add > 0 and available_trait_types:
                     trait = random.choice(available_trait_types)
                     available_trait_types.remove(trait)
                     if not trait_tracker.can_add_trait(trait,
                                                        display_warn=False):
                         continue
                     trait_tracker.add_trait(trait)
                     num_to_add -= 1
示例#2
0
 def _add_traits(cls, sim_info, sim_creator=None):
     trait_tracker = sim_info.trait_tracker
     for trait in tuple(trait_tracker.personality_traits):
         sim_info.remove_trait(trait)
     if sim_creator is not None:
         for trait in sim_creator.traits:
             sim_info.add_trait(trait)
     for trait in cls._traits.explicit:
         sim_info.add_trait(trait)
     if cls._traits.num_random:
         num_to_add = cls._traits.num_random.random_int()
         if num_to_add > 0:
             trait_manager = services.trait_manager()
             available_trait_types = {
                 trait
                 for trait in trait_manager.types.values()
                 if trait.is_personality_trait
                 if not sim_info.has_trait(trait)
             }
             available_trait_types -= cls._traits.blacklist
             available_trait_types -= set(cls._traits.explicit)
             available_trait_types = list(available_trait_types)
             while num_to_add > 0:
                 while available_trait_types:
                     trait = random.choice(available_trait_types)
                     available_trait_types.remove(trait)
                     if not trait_tracker.can_add_trait(trait):
                         continue
                     sim_info.add_trait(trait)
                     num_to_add -= 1
示例#3
0
class CreatePuddlesLootOp(BaseTargetedLootOperation):
    __qualname__ = 'CreatePuddlesLootOp'
    FACTORY_TUNABLES = {
        'description':
        '\n            This loot will create puddles based on a tuned set of chances.\n            ',
        'trait_puddle_factory':
        TunableList(
            TunableTuple(
                trait=TunableReference(manager=services.trait_manager()),
                puddle_factory=TunablePuddleFactory(
                    description=
                    '\n                The chance of creating a puddle of various sizes.\n                '
                ))),
        'default_puddle_factory':
        TunablePuddleFactory(
            description=
            '\n            This set of chances will be used if the sim creating the puddle does\n            not match any of the traits in the trait_puddle_chances tuning list.\n            '
        ),
        'max_distance':
        Tunable(
            description=
            '\n                Maximum distance from the source object a puddle can be spawned.\n                If no position is found within this distance no puddle will be \n                made.\n                ',
            tunable_type=float,
            default=2.5)
    }

    def __init__(self, trait_puddle_factory, default_puddle_factory,
                 max_distance, **kwargs):
        super().__init__(**kwargs)
        self.trait_puddle_factory = trait_puddle_factory
        self.default_puddle_factory = default_puddle_factory
        self.max_distance = max_distance

    def _apply_to_subject_and_target(self, subject, target, resolver):
        puddle_factory = self.default_puddle_factory
        trait_tracker = subject.trait_tracker
        for item in self.trait_puddle_factory:
            while trait_tracker.has_trait(item.trait):
                puddle_factory = item.puddle_factory
                break
        puddle = self.create_puddle_from_factory(puddle_factory)
        if puddle is not None:
            target_obj = target.get_sim_instance() if target.is_sim else target
            puddle.place_puddle(target_obj, self.max_distance)

    def create_puddle_from_factory(self, puddle_factory):
        value = sims4.random.weighted_random_item([
            (puddle_factory.none, PuddleSize.NoPuddle),
            (puddle_factory.small, PuddleSize.SmallPuddle),
            (puddle_factory.medium, PuddleSize.MediumPuddle),
            (puddle_factory.large, PuddleSize.LargePuddle)
        ])
        return create_puddle(value, puddle_factory.liquid)
示例#4
0
 def advance_age(self):
     current_age = self.age
     next_age = self.get_next_age(current_age)
     self._relationship_tracker.update_bits_on_age_up(current_age)
     self.change_age(next_age, current_age)
     services.get_event_manager().process_event(TestEvent.AgedUp,
                                                sim_info=self)
     school_data = self.get_school_data()
     if school_data is not None:
         school_data.update_school_data(self, create_homework=True)
     if self.is_npc:
         if self.is_child or self.is_teen:
             available_aspirations = []
             aspiration_track_manager = services.get_instance_manager(
                 sims4.resources.Types.ASPIRATION_TRACK)
             aspiration_tracker = self.aspiration_tracker
             for aspiration_track in aspiration_track_manager.types.values(
             ):
                 track_available = not aspiration_track.is_hidden_unlockable
                 if not track_available:
                     if aspiration_tracker is not None:
                         track_available = aspiration_tracker.is_aspiration_track_visible(
                             aspiration_track)
                 if track_available:
                     if aspiration_track.is_child_aspiration_track:
                         if self.is_child:
                             available_aspirations.append(aspiration_track)
                             if self.is_teen:
                                 available_aspirations.append(
                                     aspiration_track)
                     elif self.is_teen:
                         available_aspirations.append(aspiration_track)
             self.primary_aspiration = random.choice(available_aspirations)
         trait_tracker = self.trait_tracker
         empty_trait_slots = trait_tracker.empty_slot_number
         if empty_trait_slots:
             available_traits = [
                 trait for trait in services.trait_manager().types.values()
                 if trait.is_personality_trait
             ]
             while empty_trait_slots > 0:
                 while available_traits:
                     trait = random.choice(available_traits)
                     available_traits.remove(trait)
                     if not trait_tracker.can_add_trait(trait):
                         continue
                     if self.add_trait(trait):
                         empty_trait_slots -= 1
     age_transition = self.get_age_transition_data(next_age)
     age_transition.apply_aging_transition_loot(self)
     self._create_additional_statistics()
     life_skills_trait_gained = self._apply_life_skill_traits()
     return life_skills_trait_gained
 def build_msg(self, additional_tokens=(), trait_overrides_for_baby=None, gender_overrides_for_baby=None, **kwargs):
     msg = Dialog_pb2.SimPersonalityAssignmentDialog()
     msg.sim_id = self._assignment_sim_info.id
     dialog_msg = super().build_msg(additional_tokens=additional_tokens, **kwargs)
     msg.dialog = dialog_msg
     msg.secondary_title = self._build_localized_string_msg(self.secondary_title, *additional_tokens)
     msg.age_description = self._build_localized_string_msg(self.age_description, *additional_tokens)
     if self.naming_title_text is not None:
         msg.naming_title_text = self._build_localized_string_msg(self.naming_title_text, *additional_tokens)
     if gender_overrides_for_baby is None:
         gender = self._assignment_sim_info.gender
     else:
         gender = gender_overrides_for_baby
     msg.is_female = gender == Gender.FEMALE
     if self.aspirations_and_trait_assignment is not None:
         msg.aspirations_and_trait_assignment_text = self._build_localized_string_msg(self.aspirations_and_trait_assignment, *additional_tokens)
         if trait_overrides_for_baby is None:
             empty_slots = self._assignment_sim_info.trait_tracker.empty_slot_number
             current_personality_traits = self._assignment_sim_info.trait_tracker.personality_traits
         else:
             empty_slots = 0
             current_personality_traits = trait_overrides_for_baby
         msg.available_trait_slots = empty_slots
         for current_personality_trait in current_personality_traits:
             msg.current_personality_trait_ids.append(current_personality_trait.guid64)
         msg.available_trait_slots = empty_slots
         if empty_slots != 0:
             for trait in services.trait_manager().types.values():
                 if not trait.is_personality_trait:
                     pass
                 if not trait.test_sim_info(self._assignment_sim_info):
                     pass
                 for current_personality_trait in current_personality_traits:
                     while trait.guid64 == current_personality_trait.guid64 or trait.is_conflicting(current_personality_trait):
                         break
                 msg.available_personality_trait_ids.append(trait.guid64)
         if trait_overrides_for_baby is None and (self._assignment_sim_info.is_child or self._assignment_sim_info.is_teen):
             aspiration_track_manager = services.get_instance_manager(sims4.resources.Types.ASPIRATION_TRACK)
             while True:
                 for aspiration_track in aspiration_track_manager.types.values():
                     if aspiration_track.is_child_aspiration_track:
                         if self._assignment_sim_info.is_child:
                             msg.available_aspiration_ids.append(aspiration_track.guid64)
                             while self._assignment_sim_info.is_teen:
                                 msg.available_aspiration_ids.append(aspiration_track.guid64)
                     else:
                         while self._assignment_sim_info.is_teen:
                             msg.available_aspiration_ids.append(aspiration_track.guid64)
     return msg
示例#6
0
 def _add_traits(cls, sim_info):
     trait_tracker = sim_info.trait_tracker
     for trait in tuple(trait_tracker.personality_traits):
         trait_tracker.remove_trait(trait)
     for trait in cls._traits.explicit:
         trait_tracker.add_trait(trait)
     if cls._traits.num_random:
         num_to_add = cls._traits.num_random.random_int()
         if num_to_add > 0:
             trait_manager = services.trait_manager()
             available_trait_types = set([trait for trait in trait_manager.types.values() if trait.is_personality_trait])
             available_trait_types = list(available_trait_types - cls._traits.blacklist - set(cls._traits.explicit))
             while True:
                 while num_to_add > 0 and available_trait_types:
                     trait = random.choice(available_trait_types)
                     available_trait_types.remove(trait)
                     if not trait_tracker.can_add_trait(trait, display_warn=False):
                         continue
                     trait_tracker.add_trait(trait)
                     num_to_add -= 1
示例#7
0
class VisitingTuning:
    ALWAYS_WELCOME_TRAITS = TunableList(description='\n        Traits that will guarantee that after the Sim is welcomed into a \n        household, it will always be automatically welcomed if he/she comes\n        back.\n        i.e. Vampires are always welcomed after being welcomed once.\n        ', tunable=TunableReference(description='\n            Trait reference to make the Sim always be welcomed after they \n            are welcomed once.\n            ', manager=services.trait_manager(), pack_safe=True))
class PickServiceNpcSuperInteraction(PickerSuperInteraction):
    INSTANCE_TUNABLES = {
        'service_npcs':
        TunableList(
            description=
            '\n            A list of the service npcs that will show up in the dialog picker\n            ',
            tunable=TunableTuple(
                description=
                '\n                Tuple of service npcs data about those NPCs being pickable.\n                ',
                service_npc=TunableReference(
                    description=
                    '\n                    The service npcs that will show up in the picker.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SERVICE_NPC),
                    class_restrictions=(ServiceNpcHireable, ),
                    pack_safe=True),
                already_hired_tooltip=TunableLocalizedStringFactory(
                    description=
                    '\n                    Tooltip that displays if the service has already been\n                    hired.\n                    '
                ),
                tests=event_testing.tests.TunableGlobalTestSet(
                    description=
                    '\n                    A set of tests that determine if this service npc will show up\n                    as available or greyed out in the picker.\n                    '
                )),
            tuning_group=GroupNames.PICKERTUNING),
        'non_service_npcs':
        TunableList(
            description=
            "\n            A List of non service NPCs that can be hired using the\n            'Hire A Service' UI.\n            ",
            tunable=TunableTuple(
                description=
                "\n                The Data needed to display the non service NPC in the \n                'Hire A Service' UI.\n                ",
                icon=TunableIconAllPacks(
                    description=
                    "\n                    The icon to be displayed in 'Hire a Service' UI\n                    ",
                    tuning_group=GroupNames.UI),
                name=TunableLocalizedStringFactory(
                    description=
                    "\n                    The name to be displayed for this NPC in the 'Hire a Service'\n                    UI.\n                    "
                ),
                cost_string=TunableVariant(
                    description=
                    '\n                    When enabled, the tuned string will be shown as the cost\n                    of hiring this NPC.\n                    ',
                    cost_amount=Tunable(
                        description='\n                        ',
                        tunable_type=int,
                        default=0),
                    no_cost_string=TunableLocalizedStringFactory(
                        description=
                        "\n                        The description to be used for this NPC in the \n                        if there isn't a cost associated with it\n                        "
                    ),
                    locked_args={'disabled': None},
                    default='disabled'),
                hire_interaction=TunableReference(
                    description=
                    '\n                    The affordance to push the sim making the call when hiring this\n                    service npc from a picker dialog from the phone.\n                    ',
                    manager=services.affordance_manager(),
                    pack_safe=True),
                tests=event_testing.tests.TunableGlobalTestSet(
                    description=
                    '\n                    A set of global tests that are always run before other tests. All\n                    tests must pass in order for the interaction to run.\n                    '
                ),
                free_service_traits=TunableList(
                    description=
                    '\n                    If any Sim in the household has one of these traits, the \n                    non service npc will be free.\n                    ',
                    tunable=TunableReference(manager=services.trait_manager(),
                                             pack_safe=True),
                    unique_entries=True))),
        'display_price_flat_rate':
        TunableLocalizedStringFactory(
            description=
            '\n            Formatting for cost of the service if it has just a one time flat fee.\n            Parameters: 0 is flat rate cost of the service\n            ',
            tuning_group=GroupNames.PICKERTUNING),
        'display_price_hourly_cost':
        TunableLocalizedStringFactory(
            description=
            '\n            Formatting for cost of the service if it is purely hourly\n            Parameters: 0 is hourly cost of the service\n            ',
            tuning_group=GroupNames.PICKERTUNING),
        'display_price_fee_and_hourly_cost':
        TunableLocalizedStringFactory(
            description=
            '\n            Formatting for cost of the service if it has an upfront cost AND an\n            hourly cost\n            Parameters: 0 is upfront cost of service. 1 is hourly cost of service\n            ',
            tuning_group=GroupNames.PICKERTUNING),
        'display_price_free':
        TunableLocalizedString(
            description=
            '\n            Description text if the service has zero upfront cost and zero hourly cost.\n            ',
            tuning_group=GroupNames.PICKERTUNING)
    }

    def _run_interaction_gen(self, timeline):
        self._show_picker_dialog(self.sim, target_sim=self.sim)
        return True
        yield

    class _ServiceNpcRecurringPair:
        def __init__(self, service_npc_type, recurring):
            self.service_npc_type = service_npc_type
            self.recurring = recurring
            self.__name__ = '{} recurring: {}'.format(self.service_npc_type,
                                                      self.recurring)

    @flexmethod
    def picker_rows_gen(cls, inst, target, context, **kwargs):
        inst_or_cls = cls if inst is None else inst
        service_npc_data_tuples = [
            service_npc_data for service_npc_data in inst_or_cls.service_npcs
        ]
        for service_npc_data in service_npc_data_tuples:
            service_npc_type = service_npc_data.service_npc
            household = context.sim.household
            service_record = household.get_service_npc_record(
                service_npc_type.guid64, add_if_no_record=False)
            is_enabled = service_record is None or not service_record.hired
            if not is_enabled:
                tooltip = service_npc_data.already_hired_tooltip
            else:
                tooltip = None
            allows_recurring = service_npc_type._recurring is not None
            display_name = service_npc_type.display_name if not allows_recurring else service_npc_type._recurring.one_time_name
            tag = PickServiceNpcSuperInteraction._ServiceNpcRecurringPair(
                service_npc_type, recurring=False)
            if any(
                    sim.trait_tracker.has_any_trait(
                        service_npc_type.free_service_traits)
                    for sim in household.sim_info_gen()):
                display_description = inst_or_cls.display_price_free
            elif not not service_npc_type.cost_up_front > 0 and service_npc_type.cost_hourly > 0:
                display_description = inst_or_cls.display_price_fee_and_hourly_cost(
                    service_npc_type.cost_up_front,
                    service_npc_type.cost_hourly)
            elif service_npc_type.cost_up_front > 0:
                display_description = inst_or_cls.display_price_flat_rate(
                    service_npc_type.cost_up_front)
            elif service_npc_type.cost_hourly > 0:
                display_description = inst_or_cls.display_price_hourly_cost(
                    service_npc_type.cost_hourly)
            else:
                display_description = inst_or_cls.display_price_free
            resolver = inst_or_cls.get_resolver(target,
                                                context,
                                                inst_or_cls,
                                                search_for_tooltip=True)
            result = service_npc_data.tests.run_tests(resolver,
                                                      search_for_tooltip=True)
            is_enabled = result == TestResult.TRUE
            tooltip = result.tooltip
            if not (not is_enabled or not is_enabled) and tooltip is None:
                continue
            row = ui.ui_dialog_picker.ObjectPickerRow(
                is_enable=is_enabled,
                name=display_name,
                icon=service_npc_type.icon,
                row_description=display_description,
                tag=tag,
                row_tooltip=tooltip)
            yield row
            if allows_recurring:
                tag = PickServiceNpcSuperInteraction._ServiceNpcRecurringPair(
                    service_npc_type, recurring=True)
                row = ui.ui_dialog_picker.ObjectPickerRow(
                    is_enable=is_enabled,
                    name=service_npc_type._recurring.recurring_name,
                    icon=service_npc_type.icon,
                    row_description=display_description,
                    tag=tag)
                yield row
        for entry in cls.non_service_npcs:
            if any(
                    sim.trait_tracker.has_any_trait(entry.free_service_traits)
                    for sim in household.sim_info_gen()):
                cost_string = inst_or_cls.display_price_free
            else:
                cost_string = inst_or_cls._get_cost_string(entry)
            resolver = inst_or_cls.get_resolver(target,
                                                context,
                                                inst_or_cls,
                                                search_for_tooltip=True)
            result = entry.tests.run_tests(resolver, search_for_tooltip=True)
            if not result and result.tooltip is None:
                continue
            row = ui.ui_dialog_picker.ObjectPickerRow(
                is_enable=result == TestResult.TRUE,
                name=entry.name(),
                icon=entry.icon,
                row_description=cost_string,
                tag=entry,
                row_tooltip=result.tooltip)
            yield row

    @flexmethod
    def _get_cost_string(cls, inst, entry):
        cost_string = entry.cost_string
        if cost_string is None:
            return
        if isinstance(cost_string, int):
            return LocalizationHelperTuning.get_for_money(cost_string)
        return cost_string()

    def on_choice_selected(self, choice_tag, **kwargs):
        tag = choice_tag
        if tag is not None:
            if isinstance(
                    tag,
                    PickServiceNpcSuperInteraction._ServiceNpcRecurringPair):
                tag.service_npc_type.on_chosen_from_service_picker(
                    self, recurring=tag.recurring)
            elif tag.hire_interaction is not None:
                push_affordance = self.generate_continuation_affordance(
                    tag.hire_interaction)
                for aop in push_affordance.potential_interactions(
                        self.sim, self.context):
                    aop.test_and_execute(self.context)
class PregnancyTracker(SimInfoTracker):
    PREGNANCY_COMMODITY_MAP = TunableMapping(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            Species these commodities are intended for.\n            ',
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        value_type=TunableReference(
            description=
            '\n            The commodity reference controlling pregnancy.\n            ',
            pack_safe=True,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)))
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_ORIGIN_TRAIT_MAPPING = TunableMapping(
        description=
        '\n        A mapping from PregnancyOrigin to a set of traits to be added at the\n        start of the pregnancy, and removed at the end of the pregnancy.\n        ',
        key_type=PregnancyOrigin,
        value_type=TunableTuple(
            description=
            '\n            A tuple of the traits that should be added/removed with a pregnancy\n            that has this origin, and the content pack they are associated with.\n            ',
            traits=TunableSet(
                description=
                '\n                The traits to be added/removed.\n                ',
                tunable=Trait.TunablePackSafeReference()),
            pack=TunableEnumEntry(
                description=
                '\n                The content pack associated with this set of traits. If the pack\n                is uninstalled, the pregnancy will be auto-completed.\n                ',
                tunable_type=Pack,
                default=Pack.BASE_GAME)))
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the\n                offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply\n                to the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B -\n                {1.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    GENDER_CHANCE_STAT = TunableReference(
        description=
        '\n        A commodity that determines the chance that an offspring is female. The\n        minimum value guarantees the offspring is male, whereas the maximum\n        value guarantees it is female.\n        ',
        manager=services.statistic_manager())
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )
    AT_BIRTH_TESTS = TunableGlobalTestSet(
        description=
        '\n        Tests to run between the pregnant sim and their partner, at the time of\n        birth. If any test fails, the the partner sim will not be set as the\n        other parent. This is intended to prevent modifications to the partner\n        sim during the time between impregnation and birth that would make the\n        partner sim an invalid parent (age too young, relationship incestuous, etc).\n        '
    )
    PREGNANCY_ORIGIN_MODIFIERS = TunableMapping(
        description=
        '\n        Define any modifiers that, given the origination of the pregnancy,\n        affect certain aspects of the generated offspring.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The origin of the pregnancy.\n            ',
            tunable_type=PregnancyOrigin,
            default=PregnancyOrigin.DEFAULT,
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            The aspects of the pregnancy modified specifically for the specified\n            origin.\n            ',
            default_relationships=TunableTuple(
                description=
                '\n                Override default relationships for the parents.\n                ',
                father_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the father.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the father.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember)),
                mother_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the mother.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the mother.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember))),
            trait_entries=TunableList(
                description=
                '\n                Sets of traits that might be randomly applied to each generated\n                offspring. Each group is individually randomized.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A set of random traits. Specify a chance that a trait from\n                    the group is selected, and then specify a set of traits.\n                    Only one trait from this group may be selected. If the\n                    chance is less than 100%, no traits could be selected.\n                    ',
                    chance=TunablePercent(
                        description=
                        '\n                        The chance that a trait from this set is selected.\n                        ',
                        default=100),
                    traits=TunableList(
                        description=
                        '\n                        The set of traits that might be applied to each\n                        generated offspring. Specify a weight for each trait\n                        compared to other traits in the same set.\n                        ',
                        tunable=TunableTuple(
                            description=
                            '\n                            A weighted trait that might be applied to the\n                            generated offspring. The weight is relative to other\n                            entries within the same set.\n                            ',
                            weight=Tunable(
                                description=
                                '\n                                The relative weight of this trait compared to\n                                other traits within the same set.\n                                ',
                                tunable_type=float,
                                default=1),
                            trait=Trait.TunableReference(
                                description=
                                '\n                                A trait that might be applied to the generated\n                                offspring.\n                                ',
                                pack_safe=True)))))))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._clear_pregnancy_data()
        self._completion_callback_listener = None
        self._completion_alarm_handle = None

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    @property
    def offspring_count_override(self):
        return self._offspring_count_override

    @offspring_count_override.setter
    def offspring_count_override(self, value):
        self._offspring_count_override = value

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self,
                        parent_a,
                        parent_b,
                        pregnancy_origin=PregnancyOrigin.DEFAULT):
        if self.is_pregnant:
            return
        if not parent_a.incest_prevention_test(parent_b):
            return
        self._seed = random.randint(1, MAX_UINT32)
        self._parent_ids = (parent_a.id, parent_b.id)
        self._offspring_data = []
        self._origin = pregnancy_origin
        self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant:
            if not self._is_enabled:
                pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
                    self._sim_info.species)
                tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
                pregnancy_commodity = tracker.get_statistic(
                    pregnancy_commodity_type, add=True)
                pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
                threshold = sims4.math.Threshold(pregnancy_commodity.max_value,
                                                 operator.ge)
                self._completion_callback_listener = tracker.create_and_add_listener(
                    pregnancy_commodity.stat_type, threshold,
                    self._on_pregnancy_complete)
                if threshold.compare(pregnancy_commodity.get_value()):
                    self._on_pregnancy_complete()
                tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
                tracker.add_statistic(self.GENDER_CHANCE_STAT)
                self._sim_info.add_trait(self.PREGNANCY_TRAIT)
                traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
                    self._origin)
                if traits_pack_tuple is not None:
                    for trait in traits_pack_tuple.traits:
                        self._sim_info.add_trait(trait)
                self._is_enabled = True

    def _on_pregnancy_complete(self, *_, **__):
        if not self.is_pregnant:
            return
        if self._sim_info.is_npc:
            current_zone = services.current_zone()
            if not current_zone.is_zone_running or self._sim_info.is_instanced(
                    allow_hidden_flags=ALL_HIDDEN_REASONS):
                if self._completion_alarm_handle is None:
                    self._completion_alarm_handle = alarms.add_alarm(
                        self,
                        clock.interval_in_sim_minutes(1),
                        self._on_pregnancy_complete,
                        repeating=True,
                        cross_zone=True)
            else:
                self._create_and_name_offspring()
                self._show_npc_dialog()
                self.clear_pregnancy()

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def _clear_pregnancy_data(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        self._offspring_count_override = None
        self._origin = PregnancyOrigin.DEFAULT
        self._is_enabled = False

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
            self._sim_info.species)
        tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
        if tracker is not None:
            stat = tracker.get_statistic(pregnancy_commodity_type, add=True)
            if stat is not None:
                stat.set_value(stat.min_value)
                stat.remove_statistic_modifier(self.PREGNANCY_RATE)
            if self._completion_callback_listener is not None:
                tracker.remove_listener(self._completion_callback_listener)
                self._completion_callback_listener = None
        tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
        if tracker is not None:
            tracker.remove_statistic(self.GENDER_CHANCE_STAT)
        if self._sim_info.has_trait(self.PREGNANCY_TRAIT):
            self._sim_info.remove_trait(self.PREGNANCY_TRAIT)
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None:
            for trait in traits_pack_tuple.traits:
                if self._sim_info.has_trait(trait):
                    self._sim_info.remove_trait(trait)
        if self._completion_alarm_handle is not None:
            alarms.cancel_alarm(self._completion_alarm_handle)
            self._completion_alarm_handle = None
        self.clear_pregnancy_visuals()
        self._clear_pregnancy_data()

    def _create_and_name_offspring(self, on_create=None):
        self.create_offspring_data()
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)
            sim_info = self.create_sim_info(offspring_data)
            if on_create is not None:
                on_create(sim_info)

    def validate_partner(self):
        impregnator = self.get_partner()
        if impregnator is None:
            return
        resolver = DoubleSimResolver(self._sim_info, impregnator)
        if not self.AT_BIRTH_TESTS.run_tests(resolver):
            self._parent_ids = (self._sim_info.id, self._sim_info.id)

    def create_sim_info(self, offspring_data):
        self.validate_partner()
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(age=offspring_data.age,
                                 gender=offspring_data.gender,
                                 species=offspring_data.species,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        sim_info.world_id = services.get_persistence_service(
        ).get_world_id_from_zone(zone_id)
        for trait in tuple(sim_info.trait_tracker.personality_traits):
            sim_info.remove_trait(trait)
        for trait in offspring_data.traits:
            sim_info.add_trait(trait)
        sim_info.apply_genetics(parent_a,
                                parent_b,
                                seed=offspring_data.genetics)
        sim_info.resend_extended_species()
        sim_info.resend_physical_attributes()
        default_track_overrides = {}
        mother = parent_a if parent_a.gender == Gender.FEMALE else parent_b
        father = parent_a if parent_a.gender == Gender.MALE else parent_b
        if self._origin in self.PREGNANCY_ORIGIN_MODIFIERS:
            father_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.father_override
            if father_override is not None:
                default_track_overrides[father] = father_override
            mother_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.mother_override
            if mother_override is not None:
                default_track_overrides[mother] = mother_override
        self.initialize_sim_info(
            sim_info,
            parent_a,
            parent_b,
            default_track_overrides=default_track_overrides)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info,
                            parent_a,
                            parent_b,
                            default_track_overrides=None):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        sim_info.set_default_relationships(
            reciprocal=True, default_track_overrides=default_track_overrides)
        services.sim_info_manager().set_default_genealogy(
            sim_infos=(sim_info, ))
        parent_generation = max(
            parent_a.generation,
            parent_b.generation if parent_b is not None else 0)
        sim_info.generation = parent_generation + 1 if sim_info.is_played_sim else parent_generation
        services.get_event_manager().process_event(TestEvent.GenerationCreated,
                                                   sim_info=sim_info)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.add_selectable_sim_info(sim_info)
        parent_b_sim_id = parent_b.sim_id if parent_b is not None else 0
        RelgraphService.relgraph_add_child(parent_a.sim_id, parent_b_sim_id,
                                           sim_info.sim_id)

    @classmethod
    def select_traits_for_offspring(cls,
                                    offspring_data,
                                    parent_a,
                                    parent_b,
                                    num_traits,
                                    origin=PregnancyOrigin.DEFAULT,
                                    random=random):
        traits = []
        personality_trait_slots = num_traits

        def _add_trait_if_possible(selected_trait):
            nonlocal personality_trait_slots
            if selected_trait in traits:
                return False
            if any(t.is_conflicting(selected_trait) for t in traits):
                return False
            if selected_trait.is_personality_trait:
                if not personality_trait_slots:
                    return False
                personality_trait_slots -= 1
            traits.append(selected_trait)
            return True

        if origin in cls.PREGNANCY_ORIGIN_MODIFIERS:
            trait_entries = cls.PREGNANCY_ORIGIN_MODIFIERS[
                origin].trait_entries
            for trait_entry in trait_entries:
                if random.random() >= trait_entry.chance:
                    continue
                selected_trait = pop_weighted(
                    [(t.weight, t.trait) for t in trait_entry.traits
                     if t.trait.is_valid_trait(offspring_data)],
                    random=random)
                if selected_trait is not None:
                    _add_trait_if_possible(selected_trait)
        if parent_a is not None:
            if parent_b is not None:
                for inherited_trait_entries in parent_a.trait_tracker.get_inherited_traits(
                        parent_b):
                    selected_trait = pop_weighted(
                        list(inherited_trait_entries), random=random)
                    if selected_trait is not None:
                        _add_trait_if_possible(selected_trait)
        if not personality_trait_slots:
            return traits
        personality_traits = get_possible_traits(offspring_data)
        random.shuffle(personality_traits)
        while True:
            current_trait = personality_traits.pop()
            if _add_trait_if_possible(current_trait):
                break
            if not personality_traits:
                return traits
        if not personality_trait_slots:
            return traits
        traits_a = set(parent_a.trait_tracker.personality_traits)
        traits_b = set(parent_b.trait_tracker.personality_traits)
        shared_parent_traits = list(
            traits_a.intersection(traits_b) - set(traits))
        random.shuffle(shared_parent_traits)
        while personality_trait_slots:
            while shared_parent_traits:
                current_trait = shared_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        remaining_parent_traits = list(
            traits_a.symmetric_difference(traits_b) - set(traits))
        random.shuffle(remaining_parent_traits)
        while personality_trait_slots:
            while remaining_parent_traits:
                current_trait = remaining_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        while personality_trait_slots:
            while personality_traits:
                current_trait = personality_traits.pop()
                _add_trait_if_possible(current_trait)
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        if self._offspring_count_override is not None:
            offspring_count = self._offspring_count_override
        else:
            offspring_count = pop_weighted([
                (p.weight *
                 p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
                 p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES
            ],
                                           random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        species = self._sim_info.species
        age = self._sim_info.get_birth_age()
        aging_data = AgingTuning.get_aging_data(species)
        num_personality_traits = aging_data.get_personality_trait_count(age)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender_chance_stat = self._sim_info.get_statistic(
                    self.GENDER_CHANCE_STAT)
                if gender_chance_stat is None:
                    gender_chance = 0.5
                else:
                    gender_chance = (gender_chance_stat.get_value() -
                                     gender_chance_stat.min_value) / (
                                         gender_chance_stat.max_value -
                                         gender_chance_stat.min_value)
                gender = Gender.FEMALE if r.random(
                ) < gender_chance else Gender.MALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_last_name(self._sim_info.last_name,
                                                 gender, species)
            offspring_data = PregnancyOffspringData(age,
                                                    gender,
                                                    species,
                                                    genetics,
                                                    last_name=last_name)
            (parent_a, parent_b) = self.get_parents()
            offspring_data.traits = self.select_traits_for_offspring(
                offspring_data,
                parent_a,
                parent_b,
                num_personality_traits,
                origin=self._origin)
            self._offspring_data.append(offspring_data)

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            elif any(sim.first_name == first_name
                     for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                offspring_data.gender, offspring_data.species)
        return first_name

    def assign_random_first_names_to_offspring_data(self):
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        if parent is None:
                            logger.error(
                                'Pregnancy for {} has a None parent for IDs {}. Please file a DT with a save attached.',
                                self._sim_info, ','.join(
                                    str(parent_id)
                                    for parent_id in self._parent_ids))
                            return
                        parent_instance = parent.get_sim_instance()
                        if parent_instance is not None:
                            if parent_instance.client is not None:
                                additional_tokens = list(
                                    itertools.chain(self.get_parents(),
                                                    self._offspring_data))
                                dialog = npc_dialog(
                                    parent_instance,
                                    DoubleSimResolver(additional_tokens[0],
                                                      additional_tokens[1]))
                                dialog.show_dialog(
                                    additional_tokens=additional_tokens)
                return

    def save(self):
        data = SimObjectAttributes_pb2.PersistablePregnancyTracker()
        data.seed = self._seed
        data.origin = self._origin
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        try:
            self._origin = PregnancyOrigin(data.origin)
        except KeyError:
            self._origin = PregnancyOrigin.DEFAULT
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)

    def refresh_pregnancy_data(self, on_create=None):
        if not self.is_pregnant:
            self.clear_pregnancy()
            return
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None and not is_available_pack(
                traits_pack_tuple.pack):
            self._create_and_name_offspring(on_create=on_create)
            self.clear_pregnancy()
        self.enable_pregnancy()

    def on_lod_update(self, old_lod, new_lod):
        if new_lod == SimInfoLODLevel.MINIMUM:
            self.clear_pregnancy()
示例#10
0
class DaycareTuning:
    NANNY_SERVICE_NPC = TunableReference(description='\n        The nanny service NPC. We check if this is hired to take \n        away babies on sims leaving.\n        ', manager=services.get_instance_manager(sims4.resources.Types.SERVICE_NPC))
    BUTLER_SERVICE_NPC = TunablePackSafeReference(description='\n        The butler service NPC. If selected to look after children, the butler\n        should have similar effects as the nanny with regards to Daycare.\n        ', manager=services.get_instance_manager(sims4.resources.Types.SERVICE_NPC))
    NANNY_SERVICE_NPC_DIALOG = UiDialogOkCancel.TunableFactory(description='\n        A dialog that shows up when toddlers (not babies) are left home alone\n        requiring daycare. If the player selects Ok, a Nanny NPC is hired for\n        the duration of daycare, and the player can keep playing with their\n        toddlers. If Cancel is selected, regular daycare behavior kicks in and\n        the toddlers become uncontrollable.\n        ')
    DAYCARE_TRAIT_ON_KIDS = TunableReference(description='\n        The trait that indicates a baby is at daycare.\n        ', manager=services.trait_manager())
    NANNY_TRAIT_ON_KIDS = TunableReference(description='\n        The trait that children and babies that are with the nanny have.\n        ', manager=services.trait_manager())
    SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single baby is sent to daycare. You can\n        reference this single baby by name.\n        ')
    SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple babies are sent to daycare. You can\n        not reference any of these babies by name.\n        ')
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single baby is brought back from daycare.\n        You can reference this single baby by name.\n        ')
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple babies are brought back from\n        daycare. You can not reference any of these babies by name.\n        ')
    SEND_CHILD_TO_NANNY_NOTIFICATION_SINGLE = TunableUiDialogNotificationSnippet(description='\n        The message appears when a single child is sent to the nanny. You can\n        reference this single nanny by name.\n        ')
    SEND_CHILD_TO_NANNY_NOTIFICATION_MULTIPLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple children are sent to the nanny. You\n        can not reference any of these children by name.\n        ')
    BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_SINGLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single child is brought back from the\n        nanny. You can reference this single child by name.\n        ')
    BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_MULTIPLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple children are brought back from\n        the nanny. You can not reference any of these by name.\n        ')
    GO_TO_DAYCARE_INTERACTION = TunableReference(description='\n        An interaction to push on instantiated Sims that need to go to Daycare.\n        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))
    DAYCARE_AWAY_ACTIONS = TunableMapping(description='\n        Map of commodities to away action.  When the default away action is\n        asked for we look at the ad data of each commodity and select the away\n        action linked to the commodity that is advertising the highest.\n        \n        This set of away actions is used exclusively for Sims in daycare.\n        ', key_type=TunableReference(description='\n            The commodity that we will look at the advertising value for.\n            ', manager=services.get_instance_manager(Types.STATISTIC), class_restrictions=('Commodity',)), value_type=TunableReference(description='\n            The away action that will applied if the key is the highest\n            advertising commodity of the ones listed.\n            ', manager=services.get_instance_manager(Types.AWAY_ACTION)))
示例#11
0
class Trait(HasTunableReference,
            SuperAffordanceProviderMixin,
            TargetSuperAffordanceProviderMixin,
            HasTunableLodMixin,
            MixerActorMixin,
            MixerProviderMixin,
            metaclass=HashedTunedInstanceMetaclass,
            manager=services.trait_manager()):
    EQUIP_SLOT_NUMBER_MAP = TunableMapping(
        description=
        '\n        The number of personality traits available to Sims of specific ages.\n        ',
        key_type=TunableEnumEntry(
            description="\n            The Sim's age.\n            ",
            tunable_type=sim_info_types.Age,
            default=sim_info_types.Age.YOUNGADULT),
        value_type=Tunable(
            description=
            '\n            The number of personality traits available to a Sim of the specified\n            age.\n            ',
            tunable_type=int,
            default=3),
        key_name='Age',
        value_name='Slot Number')
    PERSONALITY_TRAIT_TAG = TunableEnumEntry(
        description=
        '\n        The tag that marks a trait as a personality trait.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    DAY_NIGHT_TRACKING_BUFF_TAG = TunableEnumWithFilter(
        description=
        '\n        The tag that marks buffs as opting in to Day Night Tracking on traits..\n        ',
        tunable_type=tag.Tag,
        filter_prefixes=['buff'],
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    INSTANCE_TUNABLES = {
        'trait_type':
        TunableEnumEntry(
            description='\n            The type of the trait.\n            ',
            tunable_type=TraitType,
            default=TraitType.PERSONALITY,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'display_name':
        TunableLocalizedStringFactory(
            description=
            "\n            The trait's display name. This string is provided with the owning\n            Sim as its only token.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'display_name_gender_neutral':
        TunableLocalizedString(
            description=
            "\n            The trait's gender-neutral display name. This string is not provided\n            any tokens, and thus can't rely on context to properly form\n            masculine and feminine forms.\n            ",
            allow_none=True,
            tuning_group=GroupNames.APPEARANCE),
        'trait_description':
        TunableLocalizedStringFactory(
            description="\n            The trait's description.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'trait_origin_description':
        TunableLocalizedString(
            description=
            "\n            A description of how the Sim obtained this trait. Can be overloaded\n            for other uses in certain cases:\n            - When the trait type is AGENT this string is the name of the \n                agency's Trade type and will be provided with the owning sim \n                as its token.\n            - When the trait type is HIDDEN and the trait is used by the CAS\n                STORIES flow, this can be used as a secondary description in \n                the CAS Stories UI. If this trait is tagged as a CAREER CAS \n                stories trait, this description will be used to explain which \n                skills are also granted with this career.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'icon':
        TunableResourceKey(
            description="\n            The trait's icon.\n            ",
            allow_none=True,
            resource_types=CompoundTypes.IMAGE,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'pie_menu_icon':
        TunableResourceKey(
            description=
            "\n            The trait's pie menu icon.\n            ",
            resource_types=CompoundTypes.IMAGE,
            default=None,
            allow_none=True,
            tuning_group=GroupNames.APPEARANCE),
        'trait_asm_overrides':
        TunableTuple(
            description=
            '\n            Tunables that will specify if a Trait will add any parameters\n            to the Sim and how it will affect their boundary conditions.\n            ',
            param_type=OptionalTunable(
                description=
                '\n                Define if this trait is parameterized as an on/off value or as\n                part of an enumeration.\n                ',
                tunable=Tunable(
                    description=
                    '\n                    The name of the parameter enumeration. For example, if this\n                    value is tailType, then the tailType actor parameter is set\n                    to the value specified in param_value, for this Sim.\n                    ',
                    tunable_type=str,
                    default=None),
                disabled_name='boolean',
                enabled_name='enum'),
            trait_asm_param=Tunable(
                description=
                "\n                The ASM parameter for this trait. If unset, it will be auto-\n                generated depending on the instance name (e.g. 'trait_Clumsy').\n                ",
                tunable_type=str,
                default=None),
            consider_for_boundary_conditions=Tunable(
                description=
                '\n                If enabled the trait_asm_param will be considered when a Sim\n                is building the goals and validating against its boundary\n                conditions.\n                This should ONLY be enabled, if we need this parameter for\n                cases like a posture transition, or boundary specific cases. \n                On regular cases like an animation outcome, this is not needed.\n                i.e. Vampire trait has an isVampire parameter set to True, so\n                when animatin out of the coffin it does different get in/out \n                animations.  When this is enabled, isVampire will be set to \n                False for every other Sim.\n                ',
                tunable_type=bool,
                default=False),
            tuning_group=GroupNames.ANIMATION),
        'ages':
        TunableSet(
            description=
            '\n            The allowed ages for this trait. If no ages are specified, then all\n            ages are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Age,
                                     default=None,
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'genders':
        TunableSet(
            description=
            '\n            The allowed genders for this trait. If no genders are specified,\n            then all genders are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Gender,
                                     default=None,
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'species':
        TunableSet(
            description=
            '\n            The allowed species for this trait. If not species are specified,\n            then all species are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Species,
                                     default=Species.HUMAN,
                                     invalid_enums=(Species.INVALID, ),
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'conflicting_traits':
        TunableList(
            description=
            '\n            Conflicting traits for this trait. If the Sim has any of the\n            specified traits, then they are not allowed to be equipped with this\n            one.\n            \n            e.g.\n             Family Oriented conflicts with Hates Children, and vice-versa.\n            ',
            tunable=TunableReference(manager=services.trait_manager(),
                                     pack_safe=True),
            export_modes=ExportModes.All,
            tuning_group=GroupNames.AVAILABILITY),
        'is_npc_only':
        Tunable(
            description=
            '\n            If checked, this trait will get removed from Sims that have a home\n            when the zone is loaded or whenever they switch to a household that\n            has a home zone.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.AVAILABILITY),
        'cas_selected_icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed in CAS when this trait has already been applied\n            to a Sim.\n            ',
            resource_types=CompoundTypes.IMAGE,
            default=None,
            allow_none=True,
            export_modes=(ExportModes.ClientBinary, ),
            tuning_group=GroupNames.CAS),
        'cas_idle_asm_key':
        TunableInteractionAsmResourceKey(
            description=
            '\n            The ASM to use for the CAS idle.\n            ',
            default=None,
            allow_none=True,
            category='asm',
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'cas_idle_asm_state':
        Tunable(
            description=
            '\n            The state to play for the CAS idle.\n            ',
            tunable_type=str,
            default=None,
            source_location='cas_idle_asm_key',
            source_query=SourceQueries.ASMState,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'cas_trait_asm_param':
        Tunable(
            description=
            '\n            The ASM parameter for this trait for use with CAS ASM state machine,\n            driven by selection of this Trait, i.e. when a player selects the a\n            romantic trait, the Flirty ASM is given to the state machine to\n            play. The name tuned here must match the animation state name\n            parameter expected in Swing.\n            ',
            tunable_type=str,
            default=None,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'tags':
        TunableList(
            description=
            "\n            The associated categories of the trait. Need to distinguish among\n            'Personality Traits', 'Achievement Traits' and 'Walkstyle\n            Traits'.\n            ",
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID),
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'sim_info_fixup_actions':
        TunableList(
            description=
            '\n            A list of fixup actions which will be performed on a sim_info with\n            this trait when it is loaded.\n            ',
            tunable=TunableVariant(
                career_fixup_action=_SimInfoCareerFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to set a career with a specific level.\n                    '
                ),
                skill_fixup_action=_SimInfoSkillFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to set a skill with a specific level.\n                    '
                ),
                unlock_fixup_action=_SimInfoUnlockFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to unlock certain things for a Sim\n                    '
                ),
                perk_fixup_action=_SimInfoPerkFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to grant perks to a Sim. It checks perk required\n                    unlock tuning and unlocks prerequisite perks first.\n                    '
                ),
                default='career_fixup_action'),
            tuning_group=GroupNames.CAS),
        'sim_info_fixup_actions_timing':
        TunableEnumEntry(
            description=
            "\n            This is DEPRECATED, don't tune this field. We usually don't do trait-based\n            fixup unless it's related to CAS stories. We keep this field only for legacy\n            support reason.\n            \n            This is mostly to optimize performance when applying fix-ups to\n            a Sim.  We ideally would not like to spend time scanning every Sim \n            on every load to see if they need fixups.  Please be sure you \n            consult a GPE whenever you are creating fixup tuning.\n            ",
            tunable_type=SimInfoFixupActionTiming,
            default=SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD,
            tuning_group=GroupNames.DEPRECATED,
            deprecated=True),
        'teleport_style_interaction_to_inject':
        TunableReference(
            description=
            '\n             When this trait is added to a Sim, if a teleport style interaction\n             is specified, any time another interaction runs, we may run this\n             teleport style interaction to shorten or replace the route to the \n             target.\n             ',
            manager=services.get_instance_manager(
                sims4.resources.Types.INTERACTION),
            class_restrictions=('TeleportStyleSuperInteraction', ),
            allow_none=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'interactions':
        OptionalTunable(
            description=
            '\n            Mixer interactions that are available to Sims equipped with this\n            trait.\n            ',
            tunable=ContentSet.TunableFactory(locked_args={
                'phase_affordances': frozendict(),
                'phase_tuning': None
            })),
        'buffs_add_on_spawn_only':
        Tunable(
            description=
            '\n            If unchecked, buffs are added to the Sim as soon as this trait is\n            added. If checked, buffs will be added only when the Sim is\n            instantiated and removed when the Sim uninstantiates.\n            \n            General guidelines: If the buffs only matter to Sims, for example\n            buffs that alter autonomy behavior or walkstyle, this should be\n            checked.\n            ',
            tunable_type=bool,
            default=True),
        'buffs':
        TunableList(
            description=
            '\n            Buffs that should be added to the Sim whenever this trait is\n            equipped.\n            ',
            tunable=TunableBuffReference(pack_safe=True),
            unique_entries=True),
        'buffs_proximity':
        TunableList(
            description=
            '\n            Proximity buffs that are active when this trait is equipped.\n            ',
            tunable=TunableReference(manager=services.buff_manager())),
        'buff_replacements':
        TunableMapping(
            description=
            '\n            A mapping of buff replacement. If Sim has this trait on, whenever he\n            get the buff tuned in the key of the mapping, it will get replaced\n            by the value of the mapping.\n            ',
            key_type=TunableReference(
                description=
                '\n                Buff that will get replaced to apply on Sim by this trait.\n                ',
                manager=services.buff_manager(),
                reload_dependent=True,
                pack_safe=True),
            value_type=TunableTuple(
                description=
                '\n                Data specific to this buff replacement.\n                ',
                buff_type=TunableReference(
                    description=
                    '\n                    Buff used to replace the buff tuned as key.\n                    ',
                    manager=services.buff_manager(),
                    reload_dependent=True,
                    pack_safe=True),
                buff_reason=OptionalTunable(
                    description=
                    '\n                    If enabled, override the buff reason.\n                    ',
                    tunable=TunableLocalizedString(
                        description=
                        '\n                        The overridden buff reason.\n                        '
                    )),
                buff_replacement_priority=TunableEnumEntry(
                    description=
                    "\n                    The priority of this buff replacement, relative to other\n                    replacements. Tune this to be a higher value if you want\n                    this replacement to take precedence.\n                    \n                    e.g.\n                     (NORMAL) trait_HatesChildren (buff_FirstTrimester -> \n                                                   buff_FirstTrimester_HatesChildren)\n                     (HIGH)   trait_Male (buff_FirstTrimester -> \n                                          buff_FirstTrimester_Male)\n                                          \n                     In this case, both traits have overrides on the pregnancy\n                     buffs. However, we don't want males impregnated by aliens\n                     that happen to hate children to lose their alien-specific\n                     buffs. Therefore we tune the male replacement at a higher\n                     priority.\n                    ",
                    tunable_type=TraitBuffReplacementPriority,
                    default=TraitBuffReplacementPriority.NORMAL))),
        'excluded_mood_types':
        TunableList(
            TunableReference(
                description=
                '\n            List of moods that are prevented by having this trait.\n            ',
                manager=services.mood_manager())),
        'outfit_replacements':
        TunableMapping(
            description=
            "\n            A mapping of outfit replacements. If the Sim has this trait, outfit\n            change requests are intercepted to produce the tuned result. If\n            multiple traits with outfit replacements exist, the behavior is\n            undefined.\n            \n            Tuning 'Invalid' as a key acts as a fallback and applies to all\n            reasons.\n            \n            Tuning 'Invalid' as a value keeps a Sim in their current outfit.\n            ",
            key_type=TunableEnumEntry(tunable_type=OutfitChangeReason,
                                      default=OutfitChangeReason.Invalid),
            value_type=TunableEnumEntry(tunable_type=OutfitChangeReason,
                                        default=OutfitChangeReason.Invalid)),
        'disable_aging':
        OptionalTunable(
            description=
            '\n            If enabled, aging out of specific ages can be disabled.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The tuning that disables aging out of specific age groups.\n                ',
                allowed_ages=TunableSet(
                    description=
                    '\n                    A list of ages that the Sim CAN age out of. If an age is in\n                    this list then the Sim is allowed to age out of it. If an\n                    age is not in this list than a Sim is not allowed to age out\n                    of it. For example, if the list only contains Child and\n                    Teen, then a Child Sim would be able to age up to Teen and\n                    a Teen Sim would be able to age up to Young Adult. But, a\n                    Young Adult, Adult, or Elder Sim would not be able to age\n                    up.\n                    ',
                    tunable=TunableEnumEntry(Age, default=Age.ADULT)),
                tooltip=OptionalTunable(
                    description=
                    '\n                    When enabled, this tooltip will be displayed in the aging\n                    progress bar when aging is disabled because of the trait.\n                    ',
                    tunable=TunableLocalizedStringFactory(
                        description=
                        '\n                        The string that displays in the aging UI when aging up\n                        is disabled due to the trait.\n                        '
                    ))),
            tuning_group=GroupNames.SPECIAL_CASES),
        'can_die':
        Tunable(
            description=
            '\n            When set, Sims with this trait are allowed to die. When unset, Sims\n            are prevented from dying.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'culling_behavior':
        TunableVariant(
            description=
            '\n            The culling behavior of a Sim with this trait.\n            ',
            default_behavior=CullingBehaviorDefault.TunableFactory(),
            immune_to_culling=CullingBehaviorImmune.TunableFactory(),
            importance_as_npc_score=CullingBehaviorImportanceAsNpc.
            TunableFactory(),
            default='default_behavior',
            tuning_group=GroupNames.SPECIAL_CASES),
        'always_send_test_event_on_add':
        Tunable(
            description=
            '\n            If checked, will send out a test event when added to a trait\n            tracker even if the receiving sim is hidden or not instanced.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES),
        'voice_effect':
        OptionalTunable(
            description=
            '\n            The voice effect of a Sim with this trait. This is prioritized\n            against other traits with voice effects.\n            \n            The Sim may only have one voice effect at a time.\n            ',
            tunable=VoiceEffectRequest.TunableFactory()),
        'plumbbob_override':
        OptionalTunable(
            description=
            '\n            If enabled, allows a new plumbbob model to be used when a Sim has\n            this occult type.\n            ',
            tunable=PlumbbobOverrideRequest.TunableFactory()),
        'vfx_mask':
        OptionalTunable(
            description=
            '\n            If enabled when this trait is added the masks will be applied to\n            the Sim affecting the visibility of specific VFX.\n            Example: TRAIT_CHILDREN will provide a mask MASK_CHILDREN which \n            the monster battle object will only display VFX for any Sim \n            using that mask.\n            ',
            tunable=TunableEnumFlags(
                description=
                "\n                Mask that will be added to the Sim's mask when the trait is\n                added.\n                ",
                enum_type=VFXMask),
            enabled_name='apply_vfx_mask',
            disabled_name='no_vfx_mask'),
        'day_night_tracking':
        OptionalTunable(
            description=
            "\n            If enabled, allows this trait to track various aspects of day and\n            night via buffs on the owning Sim.\n            \n            For example, if this is enabled and the Sunlight Buff is tuned with\n            buffs, the Sim will get the buffs added every time they're in\n            sunlight and removed when they're no longer in sunlight.\n            ",
            tunable=DayNightTracking.TunableFactory()),
        'persistable':
        Tunable(
            description=
            '\n            If checked then this trait will be saved onto the sim.  If\n            unchecked then the trait will not be saved.\n            Example unchecking:\n            Traits that are applied for the sim being in the region.\n            ',
            tunable_type=bool,
            default=True),
        'initial_commodities':
        TunableSet(
            description=
            '\n            A list of commodities that will be added to a sim on load, if the\n            sim has this trait.\n            \n            If a given commodity is also blacklisted by another trait that the\n            sim also has, it will NOT be added.\n            \n            Example:\n            Adult Age Trait adds Hunger.\n            Vampire Trait blacklists Hunger.\n            Hunger will not be added.\n            ',
            tunable=Commodity.TunableReference(pack_safe=True)),
        'initial_commodities_blacklist':
        TunableSet(
            description=
            "\n            A list of commodities that will be prevented from being\n            added to a sim that has this trait.\n            \n            This always takes priority over any commodities listed in any\n            trait's initial_commodities.\n            \n            Example:\n            Adult Age Trait adds Hunger.\n            Vampire Trait blacklists Hunger.\n            Hunger will not be added.\n            ",
            tunable=Commodity.TunableReference(pack_safe=True)),
        'ui_commodity_sort_override':
        OptionalTunable(
            description=
            '\n            Optional list of commodities to override the default UI sort order.\n            ',
            tunable=TunableList(
                description=
                '\n                The position of the commodity in this list represents the sort order.\n                Add all possible combination of traits in the list.\n                If we have two traits which have sort override, we will implement\n                a priority system to determine which determines which trait sort\n                order to use.\n                ',
                tunable=Commodity.TunableReference())),
        'ui_category':
        OptionalTunable(
            description=
            '\n            If enabled then this trait will be displayed in a specific category\n            within the relationship panel if this trait would be displayed\n            within that panel.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The UI trait category that we use to categorize this trait\n                within the relationship panel.\n                ',
                tunable_type=TraitUICategory,
                default=TraitUICategory.PERSONALITY),
            export_modes=ExportModes.All,
            enabled_name='ui_trait_category_tag'),
        'loot_on_trait_add':
        OptionalTunable(
            description=
            '\n            If tuned, this list of loots will be applied when trait is added in game.\n            ',
            tunable=TunableList(
                description=
                '\n                List of loot to apply on the sim when this trait is added not\n                through CAS.\n                ',
                tunable=TunableReference(
                    description=
                    '\n                    Loot to apply.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.ACTION),
                    pack_safe=True))),
        'npc_leave_lot_interactions':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a set of Leave Lot and Leave Lot Must Run\n            interactions that this trait provides. NPC Sims with this trait will\n            use these interactions to leave the lot instead of the defaults.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Leave Lot Now and Leave Lot Now Must Run interactions.\n                ',
                leave_lot_now_interactions=TunableSet(
                    TunableReference(
                        description=
                        '\n                    If tuned, the Sim will consider these interaction when trying to run\n                    any "leave lot" situation.\n                    ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.INTERACTION),
                        allow_none=False,
                        pack_safe=True)),
                leave_lot_now_must_run_interactions=TunableSet(
                    TunableReference(
                        description=
                        '\n                    If tuned, the Sim will consider these interaction when trying to run\n                    any "leave lot must run" situation.\n                    ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.INTERACTION),
                        allow_none=False,
                        pack_safe=True)))),
        'hide_relationships':
        Tunable(
            description=
            '\n            If checked, then any relationships with a Sim who has this trait\n            will not be displayed in the UI. This is done by keeping the\n            relationship from having any tracks to actually track which keeps\n            it out of the UI.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.RELATIONSHIP),
        'whim_set':
        OptionalTunable(
            description=
            '\n            If enabled then this trait will offer a whim set to the Sim when it\n            is active.\n            ',
            tunable=TunableReference(
                description=
                '\n                A whim set that is active when this trait is active.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.ASPIRATION),
                class_restrictions=('ObjectivelessWhimSet', ))),
        'allow_from_gallery':
        Tunable(
            description=
            '\n            If checked, then this trait is allowed to be transferred over from\n            Sims downloaded from the gallery.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'remove_on_death':
        Tunable(
            description=
            '\n            If checked, when a Sim dies this trait will be removed.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES),
        'build_buy_purchase_tracking':
        OptionalTunable(
            description=
            '\n            If enabled, allows this trait to track various build-buy purchases\n            via event listening in the trait tracker.\n            ',
            tunable=TunableList(
                description=
                '\n                Loots to apply to the hamper when clothing pile is being put.\n                ',
                tunable=TunableReference(manager=services.get_instance_manager(
                    sims4.resources.Types.ACTION),
                                         class_restrictions=('LootActions', ),
                                         pack_safe=True)))
    }
    _asm_param_name = None
    default_trait_params = set()

    def __repr__(self):
        return '<Trait:({})>'.format(self.__name__)

    def __str__(self):
        return '{}'.format(self.__name__)

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._asm_param_name = cls.trait_asm_overrides.trait_asm_param
        if cls._asm_param_name is None:
            cls._asm_param_name = cls.__name__
        if cls.trait_asm_overrides.trait_asm_param is not None and cls.trait_asm_overrides.consider_for_boundary_conditions:
            cls.default_trait_params.add(
                cls.trait_asm_overrides.trait_asm_param)
        for (buff, replacement_buff) in cls.buff_replacements.items():
            if buff.trait_replacement_buffs is None:
                buff.trait_replacement_buffs = {}
            buff.trait_replacement_buffs[cls] = replacement_buff
        for mood in cls.excluded_mood_types:
            if mood.excluding_traits is None:
                mood.excluding_traits = []
            mood.excluding_traits.append(cls)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.display_name:
            if not cls.display_name_gender_neutral.hash:
                logger.error(
                    'Trait {} specifies a display name. It must also specify a gender-neutral display name. These must use different string keys.',
                    cls,
                    owner='BadTuning')
            if cls.display_name._string_id == cls.display_name_gender_neutral.hash:
                logger.error(
                    'Trait {} has the same string tuned for its display name and its gender-neutral display name. These must be different strings for localization.',
                    cls,
                    owner='BadTuning')
        if cls.day_night_tracking is not None:
            if not cls.day_night_tracking.sunlight_buffs and not (
                    not cls.day_night_tracking.shade_buffs
                    and not (not cls.day_night_tracking.day_buffs
                             and not cls.day_night_tracking.night_buffs)):
                logger.error(
                    'Trait {} has Day Night Tracking enabled but no buffs are tuned. Either tune buffs or disable the tracking.',
                    cls,
                    owner='BadTuning')
            else:
                tracking_buff_tag = Trait.DAY_NIGHT_TRACKING_BUFF_TAG
                if any(
                        buff for buff in cls.day_night_tracking.sunlight_buffs
                        if not buff.buff_type.has_tag(tracking_buff_tag)
                ) or (any(buff for buff in cls.day_night_tracking.shade_buffs
                          if not buff.buff_type.has_tag(tracking_buff_tag))
                      or any(buff for buff in cls.day_night_tracking.day_buffs
                             if not buff.buff_type.has_tag(tracking_buff_tag))
                      ) or any(
                          buff
                          for buff in cls.day_night_tracking.night_buffs
                          if not buff.buff_type.has_tag(tracking_buff_tag)):
                    logger.error(
                        'Trait {} has Day Night tracking with an invalid\n                    buff. All buffs must be tagged with {} in order to be\n                    used as part of Day Night Tracking. Add these buffs with the\n                    understanding that, regardless of what system added them, they\n                    will always be on the Sim when the condition is met (i.e.\n                    Sunlight Buffs always added with sunlight is out) and they will\n                    always be removed when the condition is not met. Even if another\n                    system adds the buff, they will be removed if this trait is\n                    tuned to do that.\n                    ',
                        cls, tracking_buff_tag)
        for buff_reference in cls.buffs:
            if buff_reference.buff_type.broadcaster is not None:
                logger.error(
                    'Trait {} has a buff {} with a broadcaster tuned that will never be removed. This is a potential performance hit, and a GPE should decide whether this is the best place for such.',
                    cls,
                    buff_reference,
                    owner='rmccord')
        for commodity in cls.initial_commodities:
            if not commodity.persisted_tuning:
                logger.error(
                    'Trait {} has an initial commodity {} that does not have persisted tuning.',
                    cls, commodity)

    @classproperty
    def is_personality_trait(cls):
        return cls.trait_type == TraitType.PERSONALITY

    @classproperty
    def is_aspiration_trait(cls):
        return cls.trait_type == TraitType.ASPIRATION

    @classproperty
    def is_gender_option_trait(cls):
        return cls.trait_type == TraitType.GENDER_OPTIONS

    @classproperty
    def is_ghost_trait(cls):
        return cls.trait_type == TraitType.GHOST

    @classproperty
    def is_robot_trait(cls):
        return cls.trait_type == TraitType.ROBOT

    @classmethod
    def is_valid_trait(cls, sim_info_data):
        if cls.ages and sim_info_data.age not in cls.ages:
            return False
        if cls.genders and sim_info_data.gender not in cls.genders:
            return False
        elif cls.species and sim_info_data.species not in cls.species:
            return False
        return True

    @classmethod
    def should_apply_fixup_actions(cls, fixup_source):
        if cls.sim_info_fixup_actions and cls.sim_info_fixup_actions_timing == fixup_source:
            if fixup_source != SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD:
                logger.warn(
                    'Trait {} has fixup actions not from CAS flow.This should only happen to old saves before EP08',
                    cls,
                    owner='yozhang')
            return True
        return False

    @classmethod
    def apply_fixup_actions(cls, sim_info):
        for fixup_action in cls.sim_info_fixup_actions:
            fixup_action(sim_info)

    @classmethod
    def can_age_up(cls, current_age):
        if not cls.disable_aging:
            return True
        return current_age in cls.disable_aging.allowed_ages

    @classmethod
    def is_conflicting(cls, trait):
        if trait is None:
            return False
        if cls.conflicting_traits and trait in cls.conflicting_traits:
            return True
        elif trait.conflicting_traits and cls in trait.conflicting_traits:
            return True
        return False

    @classmethod
    def get_outfit_change_reason(cls, outfit_change_reason):
        replaced_reason = cls.outfit_replacements.get(
            outfit_change_reason if outfit_change_reason is not None else
            OutfitChangeReason.Invalid)
        if replaced_reason is not None:
            return replaced_reason
        elif outfit_change_reason is not None:
            replaced_reason = cls.outfit_replacements.get(
                OutfitChangeReason.Invalid)
            if replaced_reason is not None:
                return replaced_reason
        return outfit_change_reason

    @classmethod
    def get_teleport_style_interaction_to_inject(cls):
        return cls.teleport_style_interaction_to_inject
示例#12
0
class ServiceNpcHireable(ServiceNpc):
    INSTANCE_TUNABLES = {
        'icon':
        TunableResourceKey(
            description=
            "\n        The icon to be displayed in 'Hire a Service' UI\n        ",
            resource_types=CompoundTypes.IMAGE,
            default=None,
            tuning_group=GroupNames.UI),
        'cost_up_front':
        TunableRange(
            description=
            '\n            The up front cost of this NPC per service session (AKA per day)\n            in simoleons. This is always charged for the service if the\n            service shows up.',
            tunable_type=int,
            default=0,
            minimum=0),
        'cost_hourly':
        TunableRange(
            description=
            '\n            The cost per hour of this NPC service. This is in addition to the\n            cost up front. EX: if you have a service with 50 upfront cost and\n            then 25 cost per hour. If the npc works for 1 hour, the total cost\n            is 50 + 25 = 75 simoleons.',
            tunable_type=int,
            default=50,
            minimum=0),
        'free_service_traits':
        TunableList(
            description=
            '\n            If any Sim in the household has one of these traits, the service\n            will be free.\n            ',
            tunable=TunableReference(manager=services.trait_manager(),
                                     pack_safe=True)),
        'bill_source':
        TunableEnumEntry(
            description=
            '\n            The bill_source tied to this NPC Service. The cost for the service\n            NPC will be applied to that bill_source in total cost of bills.\n            Delinquency tests are grouped by bill_source.',
            tunable_type=AdditionalBillSource,
            default=AdditionalBillSource.Miscellaneous),
        '_recurring':
        OptionalTunable(
            description=
            '\n            If enabled, when hiring this NPC, you can specify for them to be\n            regularly scheduled and come every day or hire them one time.',
            tunable=TunableTuple(
                one_time_name=TunableLocalizedString(
                    description=
                    '\n                    Display name for this Service NPC type when recurring is false.\n                    Ex: for Maid, non recurring name is: One Time Maid',
                    tuning_group=GroupNames.UI),
                recurring_name=TunableLocalizedString(
                    description=
                    '\n                    Display name for this Service NPC type when recurring is true. \n                    Ex: for Maid, recurring maid is: Scheduled Maid',
                    tuning_group=GroupNames.UI))),
        '_fake_perform_minutes_per_object':
        TunableSimMinute(
            description=
            "\n            If we're pretending this service npc went to your lot, and the fake\n            perform tuning is run on the lot, this is the number of minutes we\n            pretend it takes for the maid to clean each object.\n            ",
            default=10,
            minimum=0),
        '_fake_perform_notification':
        OptionalTunable(
            description=
            '\n            The notification to display when you return to your lot if this\n            service NPC visited your lot while you were away. The two arguments\n            available are the money charged directly to your household funds\n            (in argument 0), the money billed to your household (in argument\n            1), and the total cost (in argument 2). So, you can use {0.Money},\n            etc. in the notification.\n            ',
            tunable=TunableUiDialogNotificationSnippet()),
        'hire_interaction':
        TunableReference(
            description=
            '\n            The affordance to push the sim making the call when hiring this\n            service npc from a picker dialog from the phone.\n            ',
            manager=services.affordance_manager()),
        'bill_time_of_day':
        OptionalTunable(
            description=
            "\n            If enabled, service NPC will charge at a specified tunable time.\n            Otherwise service NPC will charge by default whenever the situation\n            ends (full time service NPC's should be collecting this way).\n            ",
            tunable=TunableTuple(
                description=
                '\n                Time of day and failure to pay notification when the active\n                household fails to pay for the service.\n                Delinquent NPC quit notification will trigger whenever\n                the NPC quits after bills go delinquent.\n                Notification arguments available are money charged directly to \n                your household funds (in argument 0), the money billed to your \n                household (in argument 1), and the total cost (in argument 2).\n                ',
                time_of_day=TunableTimeOfDay(
                    description=
                    '\n                    Time of day for butler to collect bills.\n                    ',
                    default_hour=12),
                fail_to_pay_notification=TunableUiDialogNotificationSnippet(),
                delinquent_npc_quit_notification=
                TunableUiDialogNotificationSnippet()),
            enabled_name='specify_bill_time',
            disabled_name='use_situation_end_as_bill')
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @classmethod
    def try_charge_for_service(cls, household, cost):
        for sim in household.sim_info_gen():
            if sim.trait_tracker.has_any_trait(cls.free_service_traits):
                cost = 0
                break
        if cost > household.funds.money:
            billed_amount = cost - household.funds.money
            paid_amount = household.funds.money
        else:
            billed_amount = 0
            paid_amount = cost
        first_instanced_sim = next(
            household.instanced_sims_gen(
                allow_hidden_flags=ALL_HIDDEN_REASONS), None)
        if not household.funds.try_remove(
                paid_amount,
                reason=Consts_pb2.TELEMETRY_INTERACTION_COST,
                sim=first_instanced_sim):
            billed_amount += paid_amount
            household.bills_manager.add_additional_bill_cost(
                cls.bill_source, billed_amount)
            return (0, billed_amount)
        if billed_amount > 0:
            household.bills_manager.add_additional_bill_cost(
                cls.bill_source, billed_amount)
        return (paid_amount, billed_amount)

    @classmethod
    def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):
        cost = int(time_worked_in_hours * cls.cost_hourly)
        if include_up_front_cost:
            cost += cls.cost_up_front
        return cost

    @classmethod
    def auto_schedule_on_client_connect(cls):
        return False

    @classmethod
    def fake_perform(cls, household):
        num_modified = super().fake_perform(household)
        minutes_taken = num_modified * cls._fake_perform_minutes_per_object
        time_taken = create_time_span(minutes=minutes_taken)
        total_cost = cls.get_cost(time_taken.in_hours())
        if total_cost > 0:
            (paid_amount, billed_amount) = cls.try_charge_for_service(
                household, total_cost)
        else:
            (paid_amount, billed_amount) = (0, 0)
        if cls._fake_perform_notification is not None:
            cls.display_payment_notification(household, paid_amount,
                                             billed_amount,
                                             cls._fake_perform_notification)
        return num_modified

    @classmethod
    def display_payment_notification(cls, household, paid_amount,
                                     billed_amount, notification):
        household_sim = next(
            household.instanced_sims_gen(
                allow_hidden_flags=ALL_HIDDEN_REASONS), None)
        if household_sim is not None:
            dialog = notification(household_sim)
            if dialog is not None:
                dialog.show_dialog(
                    additional_tokens=(paid_amount, billed_amount,
                                       paid_amount + billed_amount))
示例#13
0
    def advance_age(self, force_age=None) -> None:
        current_age = Age(self._base.age)
        next_age = Age(force_age) if force_age is not None else Age.next_age(current_age)
        self._relationship_tracker.update_bits_on_age_up(current_age)
        self.age_progress = 0
        self._dirty_flags = sys.maxsize
        self._base.update_for_age(next_age)
        getOutfitsPB = DistributorOps_pb2.SetSimOutfits()
        getOutfitsPB.ParseFromString(self._base.outfits)
        self._outfits.load_sim_outfits_from_cas_proto(getOutfitsPB)
        self.resend_physical_attributes()
        self._update_age_trait(next_age, current_age)
        self._select_age_trait()
        self.age = next_age
        self.init_child_skills()
        if self.is_teen:
            self.remove_child_only_features()
        sim_instance = self.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim_instance is not None:
            with telemetry_helper.begin_hook(writer_age, TELEMETRY_CHANGE_AGE, sim=sim_instance) as hook:
                hook.write_enum('agef', current_age)
                hook.write_enum('aget', next_age)
                sim_instance.schedule_element(services.time_service().sim_timeline, build_element(sim_instance._update_face_and_posture_gen))
                sim_instance._update_multi_motive_buff_trackers()
            if current_age != Age.BABY:
                self.verify_school(from_age_up=True)
                self.remove_invalid_age_based_careers(current_age)
        self.reset_age_progress()
        if self.is_npc:
            if self.is_child or self.is_teen:
                available_aspirations = []
                aspiration_track_manager = services.get_instance_manager(sims4.resources.Types.ASPIRATION_TRACK)
                for aspiration_track in aspiration_track_manager.types.values():
                    if aspiration_track.is_child_aspiration_track:
                        if self.is_child:
                            available_aspirations.append(aspiration_track.guid64)
                            while self.is_teen:
                                available_aspirations.append(aspiration_track.guid64)
                    else:
                        while self.is_teen:
                            available_aspirations.append(aspiration_track.guid64)
                self.primary_aspiration = random.choice(available_aspirations)
            trait_tracker = self.trait_tracker
            empty_trait_slots = trait_tracker.empty_slot_number
            available_traits = [trait for trait in services.trait_manager().types.values() if trait.is_personality_trait]
            while True:
                while empty_trait_slots > 0 and available_traits:
                    trait = random.choice(available_traits)
                    available_traits.remove(trait)
                    if not trait_tracker.can_add_trait(trait, display_warn=False):
                        continue
                    #ERROR: Unexpected statement:   770 POP_BLOCK  |   771 JUMP_ABSOLUTE 812

                    if trait_tracker.add_trait(trait):
                        empty_trait_slots -= 1
                        continue
                        continue
                    continue
        else:
            self.whim_tracker.validate_goals()
            services.social_service.post_aging_message(self, ready_to_age=False)
        client = services.client_manager().get_client_by_household_id(self._household_id)
        if client is None:
            return
        client.selectable_sims.notify_dirty()
示例#14
0
 def __init__(self, **kwargs):
     super().__init__(age_up_warning_notification=UiDialogNotification.TunableFactory(description='\n                Message sent to client to warn of impending age up.\n                '), age_up_available_notification=UiDialogNotification.TunableFactory(description='\n                Message sent to client to alert age up is ready.\n                '), age_transition_threshold=Tunable(description='\n                Number of Sim days required to be eligible to transition from\n                the mapping key age to the next one."\n                ', tunable_type=float, default=1), age_transition_warning=Tunable(description='\n                Number of Sim days prior to the transition a Sim will get a\n                warning of impending new age.\n                ', tunable_type=float, default=1), age_transition_delay=Tunable(description='\n                Number of Sim days after transition time elapsed before auto-\n                aging occurs.\n                ', tunable_type=float, default=1), auto_aging_actions=TunableTuple(buff=TunableReference(description='\n                    Buff that will be applied to the Sim when aging up to the\n                    current age.  This buff will be applied if the Sim\n                    auto-ages rather than if they age up with the birthday\n                    cake.\n                    ', manager=services.buff_manager()), buff_tests=TunableTestSet(description='\n                    Tests that will be run to determine if to apply the buff on\n                    the Sim.  This should be used to not apply the auto-aging\n                    under certain circumstances, example auto-aging while a\n                    birthday party is being thrown for the sim.\n                    '), description='\n                Tuning related to actions that will be applied on auto-aging\n                rather than aging up normally through the birday cake.\n                '), age_trait_awards=TunableList(description='\n                Traits available for selection to the Sim upon completing the\n                current age. They traits are presented in a menu for the player\n                to choose from.\n                ', tunable=TunableReference(services.trait_manager())), age_trait=TunableReference(description="\n                The age trait that corresponds to this Sim's age\n                ", manager=services.trait_manager()), age_transition_dialog=SimPersonalityAssignmentDialog.TunableFactory(description='\n                Dialog displayed to the player when their sim ages up.\n                ', locked_args={'phone_ring_type': PhoneRingType.NO_RING}))
示例#15
0
 def get_possible_traits(age, gender):
     return [trait for trait in services.trait_manager().types.values() if gender in trait.genders]
class PregnancyTracker:
    __qualname__ = 'PregnancyTracker'
    PREGNANCY_COMMODITY = TunableReference(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        manager=services.statistic_manager())
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    PREGNANCY_DIALOG = SimPersonalityAssignmentDialog.TunableFactory(
        description=
        "\n        The dialog that is displayed when an offspring is created. It allows the\n        player to enter a first and last name for the Sim. An additional token\n        is passed in: the offspring's Sim data.\n        ",
        text_inputs=(TEXT_INPUT_FIRST_NAME, TEXT_INPUT_LAST_NAME))
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply to \n                the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=ui.screen_slam.TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B - {1.SimFirstName}\n                ',
                tunable=ui.screen_slam.TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._last_modified = None
        self.clear_pregnancy()

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self, parent_a, parent_b):
        if not self.is_pregnant:
            self._seed = random.randint(1, MAX_UINT32)
            self._parent_ids = (parent_a.id, parent_b.id)
            self._offspring_data = []
            self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant and not self._is_enabled:
            tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
            pregnancy_commodity = tracker.get_statistic(
                self.PREGNANCY_COMMODITY, add=True)
            pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
            trait_tracker = self._sim_info.trait_tracker
            trait_tracker.add_trait(self.PREGNANCY_TRAIT)
            self._last_modified = None
            self._is_enabled = True

    def update_pregnancy(self):
        if self.is_pregnant:
            if self._last_modified is not None:
                tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
                pregnancy_commodity = tracker.get_statistic(
                    self.PREGNANCY_COMMODITY, add=True)
                if pregnancy_commodity.get_value(
                ) >= self.PREGNANCY_COMMODITY.max_value:
                    self.create_offspring_data()
                    for offspring_data in self.get_offspring_data_gen():
                        offspring_data.first_name = self._get_random_first_name(
                            offspring_data)
                        self.create_sim_info(offspring_data)
                    self._show_npc_dialog()
                    self.clear_pregnancy()
                else:
                    delta_time = services.time_service(
                    ).sim_now - self._last_modified
                    delta = self.PREGNANCY_RATE * delta_time.in_minutes()
                    pregnancy_commodity.add_value(delta)
            self._last_modified = services.time_service().sim_now

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            while tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
        stat = tracker.get_statistic(self.PREGNANCY_COMMODITY)
        if stat is not None:
            tracker.set_min(self.PREGNANCY_COMMODITY)
            stat.remove_statistic_modifier(self.PREGNANCY_RATE)
        trait_tracker = self._sim_info.trait_tracker
        if trait_tracker.has_trait(self.PREGNANCY_TRAIT):
            trait_tracker.remove_trait(self.PREGNANCY_TRAIT)
        self.clear_pregnancy_visuals()
        self._is_enabled = False

    def create_sim_info(self, offspring_data):
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(gender=offspring_data.gender,
                                 age=Age.BABY,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        generate_offspring(parent_a._base,
                           parent_b._base,
                           sim_info._base,
                           seed=offspring_data.genetics)
        sim_info.resend_physical_attributes()
        trait_tracker = sim_info.trait_tracker
        for trait in tuple(trait_tracker.personality_traits):
            trait_tracker.remove_trait(trait)
        for trait in offspring_data.traits:
            trait_tracker.add_trait(trait)
        self.initialize_sim_info(sim_info, parent_a, parent_b)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info, parent_a, parent_b):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        services.sim_info_manager()._set_default_genealogy()
        sim_info.set_default_relationships(reciprocal=True)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.selectable_sims.add_selectable_sim_info(sim_info)

    def _select_traits_for_offspring(self, gender):
        traits = []
        num_of_traits = Trait.EQUIP_SLOT_NUMBER_MAP[Age.BABY]
        if num_of_traits == 0:
            return traits
        possible_traits = Trait.get_possible_traits(Age.BABY, gender)
        random.shuffle(possible_traits)
        first_trait = possible_traits.pop()
        traits.append(first_trait)
        while len(traits) < num_of_traits:
            current_trait = possible_traits.pop()
            if not any(
                    trait.is_conflicting(current_trait) for trait in traits):
                traits.append(current_trait)
            while not possible_traits:
                break
                continue
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        offspring_count = pop_weighted(
            [(p.weight *
              p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
              p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES],
            random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender = Gender.MALE if r.random() < 0.5 else Gender.FEMALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_family_name_for_gender(
                self._sim_info.account, self._sim_info.last_name,
                gender == Gender.FEMALE)
            traits = self._select_traits_for_offspring(gender)
            self._offspring_data.append(
                PregnancyOffspringData(gender,
                                       genetics,
                                       last_name=last_name,
                                       traits=traits))

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            if any(sim.first_name == first_name
                   for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                self.account, offspring_data.is_female)
        return first_name

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            while tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        parent_instance = parent.get_sim_instance()
                        while parent_instance is not None and parent_instance.client is not None:
                            additional_tokens = list(
                                itertools.chain(self.get_parents(),
                                                self._offspring_data))
                            dialog = npc_dialog(
                                parent_instance,
                                DoubleSimResolver(additional_tokens[0],
                                                  additional_tokens[1]))
                            dialog.show_dialog(
                                additional_tokens=additional_tokens)
                return

    def save(self):
        data = protocols.PersistablePregnancyTracker()
        data.seed = self._seed
        if self._last_modified is not None:
            self.last_modified = self._last_modified.absolute_ticks()
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        if data.HasField('last_modified'):
            self._last_modified = DateAndTime(data.last_modified)
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)
示例#17
0
class Baby(GameObject, HasSimInfoBasicMixin):
    MAX_PLACEMENT_ATTEMPTS = 8
    BASSINET_EMPTY_STATE = TunableStateValueReference(
        description='\n        The state value for an empty bassinet.\n        '
    )
    BASSINET_BABY_STATE = TunableStateValueReference(
        description=
        '\n        The state value for a non-empty bassinet.\n        ')
    BASSINET_BABY_TRAIT_STATES = TunableMapping(
        description=
        "\n        Specify any object states that are determined by the baby's traits. For\n        example, tune a state with a geometry state override to handle Alien\n        babies having their own geometry state.\n        ",
        key_type=TunableReference(
            description=
            '\n            A trait that would cause babies to have a specific state..\n            ',
            manager=services.trait_manager(),
            pack_safe=True),
        value_type=TunableStateValueReference(
            description=
            '\n            The state associated with this trait.\n            ',
            pack_safe=True))
    STATUS_STATE = ObjectState.TunableReference(
        description=
        '\n        The state defining the overall status of the baby (e.g. happy, crying,\n        sleeping). We use this because we need to reapply this state to restart\n        animations after a load.\n        '
    )
    BABY_SKIN_TONE_STATE_MAPPING = TunableMapping(
        description=
        '\n        From baby skin tone enum to skin tone state mapping.\n        ',
        key_type=TunableEnumEntry(tunable_type=BabySkinTone,
                                  default=BabySkinTone.MEDIUM),
        value_type=TunableTuple(boy=TunableStateValueReference(),
                                girl=TunableStateValueReference()))
    BABY_MOOD_MAPPING = TunableMapping(
        description=
        '\n        From baby state (happy, crying, sleep) to in game mood.\n        ',
        key_type=TunableStateValueReference(),
        value_type=Mood.TunableReference())
    BABY_AGE_UP = TunableTuple(
        description=
        '\n        Multiple settings for baby age up moment.\n        ',
        age_up_affordance=TunableReference(
            description=
            '\n            The affordance to run when baby age up to kid.\n            ',
            manager=services.affordance_manager(),
            class_restrictions='SuperInteraction'),
        copy_states=TunableList(
            description=
            '\n            The list of the state we want to copy from the original baby\n            bassinet to the new bassinet to play idle.\n            ',
            tunable=TunableReference(manager=services.object_state_manager(),
                                     class_restrictions='ObjectState')),
        idle_state_value=TunableReference(
            description=
            '\n            The state value to apply on the new baby bassinet with the age up\n            special idle animation/vfx linked.\n            ',
            manager=services.object_state_manager(),
            class_restrictions='ObjectStateValue'))
    BABY_PLACEMENT_TAGS = TunableList(
        description=
        '\n        When trying to place a baby bassinet on the lot, we attempt to place it\n        near other objects on the lot. Those objects are determined in priority\n        order by this tuned list. It will try to place next to all objects of\n        the matching types, before trying to place the baby in the middle of the\n        lot, and then finally trying the mailbox. If all FGL placements fail, we\n        put the baby into the household inventory.\n        ',
        tunable=TunableEnumEntry(
            description=
            '\n            Attempt to place the baby near objects with this tag set.\n            ',
            tunable_type=Tag,
            default=Tag.INVALID))
    REMOVAL_MOMENT_STATES = TunableMapping(
        description=
        '\n        A mapping of states to removal moments. When the baby is set to\n        specified state, then the removal moment will execute and the object\n        (and Sim) will be destroyed.\n        ',
        key_type=TunableStateValueReference(
            description=
            '\n            The state that triggers the removal moment.\n            ',
            pack_safe=True),
        value_type=_BabyRemovalMoment.TunableFactory(
            description=
            '\n            The moment that will execute when the specified state is triggered.\n            '
        ))
    FAILED_PLACEMENT_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification to show if a baby could not be spawned into the world\n        because FGL failed. This is usually due to the player cluttering their\n        lot with objects. Token 0 is the baby.\n        '
    )

    @classmethod
    def get_baby_skin_tone_state(cls, sim_info):
        skin_tone_state_value = None
        baby_skin_enum = BabyTuning.get_baby_skin_tone_enum(sim_info)
        if baby_skin_enum is not None:
            if baby_skin_enum in cls.BABY_SKIN_TONE_STATE_MAPPING:
                skin_state_tuple = cls.BABY_SKIN_TONE_STATE_MAPPING[
                    baby_skin_enum]
                if sim_info.gender == Gender.FEMALE:
                    skin_tone_state_value = skin_state_tuple.girl
                elif sim_info.gender == Gender.MALE:
                    skin_tone_state_value = skin_state_tuple.boy
        return skin_tone_state_value

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sim_info = None
        self.state_component.state_trigger_enabled = False
        self.is_being_removed = False
        self.replacement_bassinet = None
        self._pending_removal_moment = None

    def get_delete_op(self, *args, **kwargs):
        if self.replacement_bassinet is not None:
            return distributor.ops.ObjectReplace(
                replacement_obj=self.replacement_bassinet)
        return super().get_delete_op(*args, **kwargs)

    def may_reserve(self, *args, **kwargs):
        if self.is_being_removed:
            return False
        return super().may_reserve(*args, **kwargs)

    def set_sim_info(self, sim_info):
        self._sim_info = sim_info
        if self._sim_info is not None:
            self.state_component.state_trigger_enabled = True
            self.enable_baby_state()

    @property
    def sim_info(self):
        return self._sim_info

    @property
    def sim_id(self):
        if self._sim_info is not None:
            return self._sim_info.sim_id
        return self.id

    def get_age_up_addordance(self):
        return Baby.BABY_AGE_UP.age_up_affordance

    def replace_for_age_up(self, interaction=None):
        if interaction is not None:
            interaction.set_target(None)
        new_bassinet = create_object(self.definition)
        new_bassinet.location = self.location
        self.replacement_bassinet = new_bassinet
        new_bassinet.set_sim_info(self.sim_info)
        new_bassinet.copy_state_values(self,
                                       state_list=Baby.BABY_AGE_UP.copy_states)
        idle_state_value = Baby.BABY_AGE_UP.idle_state_value
        new_bassinet.set_state(idle_state_value.state, idle_state_value)
        if interaction is not None:
            interaction.set_target(new_bassinet)
        self.destroy(source=self.sim_info,
                     cause='Replacing bassinet for age up.')
        return new_bassinet

    def place_in_good_location(self, position=None, routing_surface=None):
        plex_service = services.get_plex_service()
        is_active_zone_a_plex = plex_service.is_active_zone_a_plex()

        def try_to_place_bassinet(position, routing_surface=None, **kwargs):
            starting_location = placement.create_starting_location(
                position=position, routing_surface=routing_surface)
            fgl_context = placement.create_fgl_context_for_object(
                starting_location, self, **kwargs)
            (translation,
             orientation) = placement.find_good_location(fgl_context)
            if translation is not None and orientation is not None:
                if is_active_zone_a_plex and (
                        routing_surface is None
                        or plex_service.get_plex_zone_at_position(
                            translation,
                            routing_surface.secondary_id) is None):
                    return False
                else:
                    self.move_to(translation=translation,
                                 orientation=orientation)
                    if routing_surface is not None:
                        self.move_to(routing_surface=routing_surface)
                    return True
            return False

        if position is not None and try_to_place_bassinet(
                position, routing_surface=routing_surface):
            return True
        lot = services.active_lot()
        for tag in Baby.BABY_PLACEMENT_TAGS:
            for (attempt, obj) in enumerate(
                    services.object_manager().get_objects_with_tag_gen(tag)):
                position = obj.position
                routing_surface = obj.routing_surface
                if lot.is_position_on_lot(position) and try_to_place_bassinet(
                        position, routing_surface=routing_surface,
                        max_distance=10):
                    return
                if attempt >= Baby.MAX_PLACEMENT_ATTEMPTS:
                    break
        position = lot.get_default_position()
        if not try_to_place_bassinet(position):
            self.update_ownership(self.sim_info, make_sim_owner=False)
            if not build_buy.move_object_to_household_inventory(self):
                logger.error(
                    'Failed to place bassinet in household inventory.',
                    owner='rmccord')
            if self.is_selectable:
                failed_placement_notification = Baby.FAILED_PLACEMENT_NOTIFICATION(
                    self.sim_info, SingleSimResolver(self.sim_info))
                failed_placement_notification.show_dialog()

    def populate_localization_token(self, *args, **kwargs):
        if self.sim_info is not None:
            return self.sim_info.populate_localization_token(*args, **kwargs)
        logger.warn(
            'self.sim_info is None in baby.populate_localization_token',
            owner='epanero',
            trigger_breakpoint=True)
        return super().populate_localization_token(*args, **kwargs)

    def enable_baby_state(self):
        if self._sim_info is None:
            return
        self.set_state(self.BASSINET_BABY_STATE.state,
                       self.BASSINET_BABY_STATE)
        status_state = self.get_state(self.STATUS_STATE)
        self.set_state(status_state.state, status_state, force_update=True)
        skin_tone_state = self.get_baby_skin_tone_state(self._sim_info)
        if skin_tone_state is not None:
            self.set_state(skin_tone_state.state, skin_tone_state)
        for (trait, trait_state) in self.BASSINET_BABY_TRAIT_STATES.items():
            if self._sim_info.has_trait(trait):
                self.set_state(trait_state.state, trait_state)

    def empty_baby_state(self):
        self.set_state(self.BASSINET_EMPTY_STATE.state,
                       self.BASSINET_EMPTY_STATE)

    def on_state_changed(self, state, old_value, new_value, from_init):
        super().on_state_changed(state, old_value, new_value, from_init)
        removal_moment = self.REMOVAL_MOMENT_STATES.get(new_value)
        if removal_moment is not None:
            if self._sim_info is not None:
                removal_moment.execute_removal_moment(self)
            else:
                self._pending_removal_moment = removal_moment
            return
        if self.manager is not None and new_value in self.BABY_MOOD_MAPPING:
            mood = self.BABY_MOOD_MAPPING[new_value]
            mood_msg = Commodities_pb2.MoodUpdate()
            mood_msg.sim_id = self.id
            mood_msg.mood_key = mood.guid64
            mood_msg.mood_intensity = 1
            distributor.shared_messages.add_object_message(
                self, MSG_SIM_MOOD_UPDATE, mood_msg, False)

    def load_object(self, object_data, **kwargs):
        self._sim_info = services.sim_info_manager().get(self.sim_id)
        super().load_object(object_data, **kwargs)
        if self._sim_info is not None:
            services.daycare_service().refresh_daycare_status(self._sim_info)

    def _validate_location(self):
        plex_service = services.get_plex_service()
        if not plex_service.is_active_zone_a_plex():
            return
        if plex_service.get_plex_zone_at_position(self.position,
                                                  self.level) is not None:
            return
        self.place_in_good_location()

    def on_finalize_load(self):
        sim_info = services.sim_info_manager().get(self.sim_id)
        if sim_info is None or sim_info.household is not services.active_lot(
        ).get_household():
            replace_bassinet(sim_info, bassinet=self)
        else:
            self.set_sim_info(sim_info)
            self._validate_location()
            if self._pending_removal_moment is not None:
                self._pending_removal_moment.execute_removal_moment(self)
                self._pending_removal_moment = None
        super().on_finalize_load()
class RebateManager:
    __qualname__ = 'RebateManager'
    TRAIT_REBATE_MAP = TunableMapping(
        description=
        '\n        A mapping of traits and the tags of objects which provide a rebate for\n        the given trait.\n        ',
        key_type=TunableReference(
            description=
            '\n            If the Sim has this trait, any objects purchased with the given\n            tag(s) below will provide a rebate.\n            ',
            manager=services.trait_manager()),
        value_type=TunableTuple(
            description=
            '\n            The information about the rebates the player should get for having\n            the mapped trait.\n            ',
            valid_objects=TunableVariant(
                description=
                '\n                The items to which the rebate will be applied.\n                ',
                by_tag=TunableSet(
                    description=
                    '\n                    The rebate will only be applied to objects purchased with the\n                    tags in this list.\n                    ',
                    tunable=TunableEnumEntry(tag.Tag, tag.Tag.INVALID)),
                locked_args={'all_purchases': None}),
            rebate_percentage=TunablePercent(
                description=
                '\n                The percentage of the catalog price that the player will get\n                back in the rebate.\n                ',
                default=10)))
    REBATE_PAYMENT_SCHEDULE = TunableWeeklyScheduleFactory(
        description=
        '\n        The schedule when accrued rebates will be paid out.\n        '
    )
    REBATE_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification that will show when the player receives their rebate money.\n        ',
        locked_args={'text': None})
    REBATE_NOTIFICATION_HEADER = TunableLocalizedString(
        description=
        '\n        The header for the rebate notification that displays when the households\n        gets their rebate payout.\n        '
    )
    REBATE_NOTIFICATION_LINE_ITEM = TunableLocalizedStringFactory(
        description=
        '\n        Each trait that gave rebates will generate a new line item on the notification.\n        {0.String} = trait name\n        {1.Money} = amount of rebate money received from the trait.\n        '
    )

    def __init__(self, household):
        self._household = household
        self._rebates = Counter()
        self._schedule = None

    def add_rebate_for_object(self, obj):
        for (trait, rebate_info) in self.TRAIT_REBATE_MAP.items():
            rebate_percentage = rebate_info.rebate_percentage
            valid_objects = rebate_info.valid_objects
            while self._sim_in_household_has_trait(trait):
                if valid_objects is None or self._object_has_required_tags(
                        obj, valid_objects):
                    self._rebates[
                        trait] += obj.catalog_value * rebate_percentage
        if self._rebates:
            self.start_rebate_schedule()

    def _sim_in_household_has_trait(self, trait):
        return any(
            s.trait_tracker.has_trait(trait)
            for s in self._household.sim_info_gen())

    @staticmethod
    def _object_has_required_tags(obj, valid_tags):
        return set(obj.tags) & set(valid_tags)

    def clear_rebates(self):
        self._rebates.clear()

    def start_rebate_schedule(self):
        if self._schedule is None:
            self._schedule = self.REBATE_PAYMENT_SCHEDULE(
                start_callback=self._payout_rebates, schedule_immediate=False)

    def _payout_rebates(self, *_):
        if not self._rebates:
            return
        active_sim = self._household.client.active_sim
        line_item_text = LocalizationHelperTuning.get_new_line_separated_strings(
            *(self.REBATE_NOTIFICATION_LINE_ITEM(t.display_name(active_sim), a)
              for (t, a) in self._rebates.items()))
        notification_text = LocalizationHelperTuning.get_new_line_separated_strings(
            self.REBATE_NOTIFICATION_HEADER, line_item_text)
        dialog = self.REBATE_NOTIFICATION(
            active_sim, text=lambda *_, **__: notification_text)
        dialog.show_dialog()
        total_rebate_amount = sum(self._rebates.values())
        self._household.funds.add(
            total_rebate_amount,
            reason=Consts_pb2.TELEMETRY_MONEY_ASPIRATION_REWARD,
            sim=active_sim)
        self.clear_rebates()
示例#19
0
class GlobalGenderPreferenceTuning:
    GENDER_PREFERENCE = TunableMapping(
        key_type=TunableEnumEntry(
            sims.sim_info_types.Gender,
            sims.sim_info_types.Gender.MALE,
            description='The gender to index the gender preference to.'),
        value_type=TunableReference(
            services.get_instance_manager(sims4.resources.Types.STATISTIC),
            description=
            'The statistic that represents the matching gender preference'),
        description=
        'A mapping between gender and the gender preference statistic for easy lookup.'
    )
    GENDER_PREFERENCE_WEIGHTS = TunableList(
        description=
        'A weightings list for the weighted random choice of sexual preference.',
        tunable=TunableTuple(
            gender_preference=TunableEnumEntry(
                GenderPreference,
                GenderPreference.LIKES_NEITHER,
                description='The gender to index the gender preference to.'),
            weight=Tunable(int, 0, description='The minimum possible skill.'),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ))
    GENDER_PREFERENCE_MAPPING = TunableMapping(
        key_type=TunableEnumEntry(
            GenderPreference,
            GenderPreference.LIKES_NEITHER,
            description='The gender to index the gender preference to.'),
        value_type=TunableMapping(
            key_type=TunableEnumEntry(
                sims.sim_info_types.Gender,
                sims.sim_info_types.Gender.MALE,
                description='The gender to index the gender preference to.'),
            value_type=TunableSet(
                TunableReference(
                    services.get_instance_manager(
                        sims4.resources.Types.STATISTIC),
                    description=
                    'The statistic that represents the matching gender preference'
                )),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ),
        description=
        'A mapping between gender and the gender preference statistic for easy lookup.'
    )
    enable_autogeneration_same_sex_preference = False
    ENABLE_AUTOGENERATION_SAME_SEX_PREFERENCE_THRESHOLD = Tunable(
        description=
        "\n        A value that, once crossed, indicates the player's allowance of same-\n        sex relationships with townie auto-generation.\n        ",
        tunable_type=float,
        default=1.0)
    ENABLED_AUTOGENERATION_SAME_SEX_PREFERENCE_WEIGHTS = TunableList(
        description=
        '\n        An alternative weightings list for the weighted random choice of sexual\n        preference after a romantic same-sex relationship has been kindled.\n        ',
        tunable=TunableTuple(
            gender_preference=TunableEnumEntry(
                GenderPreference,
                GenderPreference.LIKES_NEITHER,
                description='The gender to index the gender preference to.'),
            weight=Tunable(int, 0, description='The minimum possible skill.'),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ))
    MALE_CLOTHING_PREFERENCE_TRAIT = TunableReference(
        description=
        '\n        The trait that signifies that this sim prefers to wear male clothing.\n        ',
        manager=services.trait_manager())
    FEMALE_CLOTHING_PREFERENCE_TRAIT = TunableReference(
        description=
        '\n        The trait that signifies that this sim prefers to wear female clothing.\n        ',
        manager=services.trait_manager())
示例#20
0
class CreatePuddlesLootOp(BaseTargetedLootOperation):
    FACTORY_TUNABLES = {
        'description':
        '\n            This loot will create puddles based on a tuned set of chances.\n            ',
        'trait_based_puddle_factory':
        OptionalTunable(
            description=
            '\n            A particpant type may be set to choose a puddle factory\n            based on traits the participant has.\n                     \n            If disabled, the default puddle factory is used.\n            ',
            tunable=TunableTuple(
                subject=TunableEnumEntry(
                    description=
                    '\n                     The participant type whose traits are checked to determine\n                     which Trait Puddle Factory to use.\n                     ',
                    tunable_type=ParticipantType,
                    default=ParticipantType.Actor),
                trait_puddle_factory=TunableList(
                    description=
                    '\n                    Ordered list of puddle factories with associated trait.\n                    Will use the first factory whose trait is on the subject.\n                    ',
                    tunable=TunableTuple(
                        trait=TunableReference(
                            manager=services.trait_manager()),
                        puddle_factory=TunablePuddleFactory(
                            description=
                            '\n                            The chance of creating a puddle of various sizes.\n                            '
                        ))))),
        'default_puddle_factory':
        TunablePuddleFactory(
            description=
            '\n            This set of chances will be used if the sim creating the puddle does\n            not match any of the traits in the trait_puddle_chances tuning list.\n            '
        ),
        'max_distance':
        Tunable(
            description=
            '\n                Maximum distance from the source object a puddle can be spawned.\n                If no position is found within this distance no puddle will be \n                made.\n                ',
            tunable_type=float,
            default=2.5),
        'locked_args': {
            'subject': None
        }
    }

    def __init__(self, trait_based_puddle_factory, default_puddle_factory,
                 max_distance, **kwargs):
        super().__init__(**kwargs)
        self.trait_based_puddle_factory = trait_based_puddle_factory
        self.default_puddle_factory = default_puddle_factory
        self.max_distance = max_distance
        self._subject = None if self.trait_based_puddle_factory is None else self.trait_based_puddle_factory.subject

    def _apply_to_subject_and_target(self, subject, target, resolver):
        puddle_factory = self.default_puddle_factory
        if subject is not None:
            trait_tracker = subject.trait_tracker
            for item in self.trait_based_puddle_factory.trait_puddle_factory:
                if trait_tracker.has_trait(item.trait):
                    puddle_factory = item.puddle_factory
                    break
        puddle = self.create_puddle_from_factory(puddle_factory)
        if puddle is not None:
            target_obj = target.get_sim_instance() if target.is_sim else target
            puddle.place_puddle(target_obj, self.max_distance)

    def create_puddle_from_factory(self, puddle_factory):
        value = sims4.random.weighted_random_item([
            (puddle_factory.none, PuddleSize.NoPuddle),
            (puddle_factory.small, PuddleSize.SmallPuddle),
            (puddle_factory.medium, PuddleSize.MediumPuddle),
            (puddle_factory.large, PuddleSize.LargePuddle)
        ])
        return create_puddle(value, puddle_factory.liquid)
示例#21
0
class DeathTracker(SimInfoTracker):
    DEATH_ZONE_ID = 0
    DEATH_TYPE_GHOST_TRAIT_MAP = TunableMapping(
        description=
        '\n        The ghost trait to be applied to a Sim when they die with a given death\n        type.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The death type to map to a ghost trait.\n            ',
            tunable_type=DeathType,
            default=DeathType.NONE),
        key_name='Death Type',
        value_type=TunableReference(
            description=
            '\n            The ghost trait to apply to a Sim when they die from the specified\n            death type.\n            ',
            manager=services.trait_manager()),
        value_name='Ghost Trait')
    DEATH_BUFFS = TunableList(
        description=
        '\n        A list of buffs to apply to Sims when another Sim dies. For example, use\n        this tuning to tune a "Death of a Good Friend" buff.\n        ',
        tunable=TunableTuple(
            test_set=TunableReference(
                description=
                "\n                The test that must pass between the dying Sim (TargetSim) and\n                the Sim we're considering (Actor). If this test passes, no\n                further test is executed.\n                ",
                manager=services.get_instance_manager(sims4.resources.Types.
                                                      SNIPPET),
                class_restrictions=('TestSetInstance', ),
                pack_safe=True),
            buff=TunableBuffReference(
                description=
                '\n                The buff to apply to the Sim.\n                ',
                pack_safe=True),
            notification=OptionalTunable(
                description=
                '\n                If enabled, an off-lot death generates a notification for the\n                target Sim. This is limited to one per death instance.\n                ',
                tunable=TunableUiDialogNotificationReference(
                    description=
                    '\n                    The notification to show.\n                    ',
                    pack_safe=True))))
    IS_DYING_BUFF = TunableReference(
        description=
        '\n        A reference to the buff a Sim is given when they are dying.\n        ',
        manager=services.buff_manager())
    DEATH_RELATIONSHIP_BIT_FIXUP_LOOT = TunableReference(
        description=
        '\n        A reference to the loot to apply to a Sim upon death.\n        \n        This is where the relationship bit fixup loots will be tuned. This\n        used to be on the interactions themselves but if the interaction was\n        reset then the bits would stay as they were. If we add more relationship\n        bits we want to clean up on death, the references Loot is the place to \n        do it.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.ACTION))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._death_type = None
        self._death_time = None

    @property
    def death_type(self):
        return self._death_type

    @property
    def death_time(self):
        return self._death_time

    @property
    def is_ghost(self):
        return self._sim_info.trait_tracker.has_any_trait(
            self.DEATH_TYPE_GHOST_TRAIT_MAP.values())

    def get_ghost_trait(self):
        return self.DEATH_TYPE_GHOST_TRAIT_MAP.get(self._death_type)

    def set_death_type(self, death_type, is_off_lot_death=False):
        is_npc = self._sim_info.is_npc
        household = self._sim_info.household
        self._sim_info.inject_into_inactive_zone(self.DEATH_ZONE_ID,
                                                 start_away_actions=False,
                                                 skip_instanced_check=True,
                                                 skip_daycare=True)
        household.remove_sim_info(self._sim_info,
                                  destroy_if_empty_household=True)
        if is_off_lot_death:
            household.pending_urnstone_ids.append(self._sim_info.sim_id)
        self._sim_info.transfer_to_hidden_household()
        clubs.on_sim_killed_or_culled(self._sim_info)
        if death_type is None:
            return
        relationship_service = services.relationship_service()
        for target_sim_info in relationship_service.get_target_sim_infos(
                self._sim_info.sim_id):
            resolver = DoubleSimResolver(target_sim_info, self._sim_info)
            for death_data in self.DEATH_BUFFS:
                if not death_data.test_set(resolver):
                    continue
                target_sim_info.add_buff_from_op(
                    death_data.buff.buff_type,
                    buff_reason=death_data.buff.buff_reason)
                if is_npc and not target_sim_info.is_npc:
                    notification = death_data.notification(target_sim_info,
                                                           resolver=resolver)
                    notification.show_dialog()
                break
        ghost_trait = DeathTracker.DEATH_TYPE_GHOST_TRAIT_MAP.get(death_type)
        if ghost_trait is not None:
            self._sim_info.add_trait(ghost_trait)
        traits = list(self._sim_info.trait_tracker.equipped_traits)
        for trait in traits:
            if trait.remove_on_death:
                self._sim_info.remove_trait(trait)
        self._death_type = death_type
        self._death_time = services.time_service().sim_now.absolute_ticks()
        self._sim_info.reset_age_progress()
        self._sim_info.resend_death_type()
        self._handle_remove_rel_bits_on_death()
        services.get_event_manager().process_event(
            test_events.TestEvent.SimDeathTypeSet, sim_info=self._sim_info)

    def _handle_remove_rel_bits_on_death(self):
        resolver = SingleSimResolver(self._sim_info)
        if self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT is not None:
            for (loot,
                 _) in self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT.get_loot_ops_gen(
                 ):
                result = loot.test_resolver(resolver)
                if result:
                    loot.apply_to_resolver(resolver)

    def clear_death_type(self):
        self._death_type = None
        self._death_time = None
        self._sim_info.resend_death_type()

    def save(self):
        if self._death_type is not None:
            data = protocols.PersistableDeathTracker()
            data.death_type = self._death_type
            data.death_time = self._death_time
            return data

    def load(self, data):
        try:
            self._death_type = DeathType(data.death_type)
        except:
            self._death_type = DeathType.NONE
        self._death_time = data.death_time

    @classproperty
    def _tracker_lod_threshold(cls):
        return SimInfoLODLevel.MINIMUM