class CareerStoryProgressionParameters(HasTunableSingletonFactory,
                                       AutoFactoryInit):
    FACTORY_TUNABLES = {
        'joining':
        OptionalTunable(
            description=
            '\n            If enabled, Sims will be able to join this career via Story\n            Progression.\n            ',
            tunable=TunableMultiplier.TunableFactory(
                description=
                '\n                The weight of a particular Sim joining this career versus all\n                other eligible Sims doing the same. A weight of zero prevents\n                the Sim from joining the career.\n                '
            )),
        'retiring':
        OptionalTunable(
            description=
            "\n            If enabled, Sims will be able to retire from this career via Story\n            Progression. This does not override the 'can_quit' flag on the\n            career tuning.\n            \n            Story Progression will attempt to have Sims retire before having\n            Sims quit.\n            ",
            tunable=TunableMultiplier.TunableFactory(
                description=
                '\n                The weight of a particular Sim retiring from this career versus\n                all other eligible Sims doing the same. A weight of zero\n                prevents the Sim from retiring from the career.\n                '
            )),
        'quitting':
        OptionalTunable(
            description=
            "\n            If enabled, Sims will be able to quit this career via Story\n            Progression. This does not override the 'can_quit' flag on the\n            career tuning.\n            ",
            tunable=TunableMultiplier.TunableFactory(
                description=
                '\n                The weight of a particular Sim quitting this career versus all\n                other eligible Sims doing the same. A weight of zero prevents\n                the Sim from quitting the career.\n                '
            ))
    }
Beispiel #2
0
class FishingData(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'weight_fish':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of tests and multipliers to apply to the weight \n            used to determine if the Sim will catch a fish instead of treasure \n            or junk. This will be used in conjunction with the Weight Junk and \n            Weight Treasure.\n            '
        ),
        'weight_junk':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of tests and multipliers to apply to the weight\n            used to determine if the Sim will catch junk instead of a fish or \n            treasure. This will be used in conjunction with the Weight Fish and \n            Weight Treasure.\n            '
        ),
        'weight_treasure':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of tests and multipliers to apply to the weight\n            used to determine if the Sim will catch a treasure instead of fish \n            or junk. This will be used in conjunction with the Weight Fish and \n            Weight Junk.\n            '
        ),
        'possible_treasures':
        TunableList(
            description=
            "\n            If the Sim catches a treasure, we'll pick one of these based on their weights.\n            Higher weighted treasures have a higher chance of being caught.\n            ",
            tunable=TunableTuple(treasure=TunableReference(
                manager=services.definition_manager(), pack_safe=True),
                                 weight=TunableMultiplier.TunableFactory())),
        'possible_fish':
        TunableList(
            description=
            "\n            If the Sim catches a fish, we'll pick one of these based on their weights.\n            Higher weighted fish have a higher chance of being caught.\n            ",
            tunable=TunableTuple(fish=TunableReference(
                manager=services.definition_manager(), pack_safe=True),
                                 weight=TunableMultiplier.TunableFactory()),
            minlength=1)
    }

    def _verify_tuning_callback(self):
        import fishing.fish_object
        for fish in self.possible_fish:
            if not issubclass(fish.fish.cls, fishing.fish_object.Fish):
                logger.error(
                    "Possible Fish on Fishing Data has been tuned but there either isn't a definition tuned for the fish, or the definition currently tuned is not a Fish.\n{}",
                    self)

    def get_possible_fish_gen(self):
        yield from self.possible_fish

    def choose_fish(self, resolver, require_bait=True):
        weighted_fish = [
            (f.weight.get_multiplier(resolver), f.fish)
            for f in self.possible_fish
            if f.fish.cls.can_catch(resolver, require_bait=require_bait)
        ]
        if weighted_fish:
            return sims4.random.weighted_random_item(weighted_fish)

    def choose_treasure(self, resolver):
        weighted_treasures = [(t.weight.get_multiplier(resolver), t.treasure)
                              for t in self.possible_treasures]
        if weighted_treasures:
            return sims4.random.weighted_random_item(weighted_treasures)
class PregnancyElement(XevtTriggeredElement):
    __qualname__ = 'PregnancyElement'
    FACTORY_TUNABLES = {
        'description':
        'Have a participant of the owning interaction become pregnant.',
        'pregnancy_subject':
        TunableEnumEntry(
            ParticipantType,
            ParticipantType.Actor,
            description=
            'The participant of this interaction that is to be impregnated.'),
        'pregnancy_parent_subject':
        TunableEnumEntry(
            ParticipantType,
            ParticipantType.TargetSim,
            description=
            'The participant of this interaction that is to be the impregnator.'
        ),
        'pregnancy_chance_and_modifiers':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of test sets and associated multipliers to apply to \n            the total chance of pregnancy.\n            '
        )
    }

    def _do_behavior(self, *args, **kwargs):
        subject = self.interaction.get_participant(self.pregnancy_subject)
        if subject is not None and subject.household.free_slot_count and random.random(
        ) < self.pregnancy_chance_and_modifiers.get_multiplier(
                self.interaction.get_resolver()):
            parent_subject = self.interaction.get_participant(
                self.pregnancy_parent_subject) or subject
            subject.sim_info.pregnancy_tracker.start_pregnancy(
                subject, parent_subject)
class PlacementHelper(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'placement_strategy_groups':
        TunableList(
            description=
            '\n            A list of ordered strategy groups. These are executed in order. If\n            any placement from the group succeeds, the placement is considered\n            terminated.\n            ',
            tunable=TunableList(
                description=
                '\n                A list of weighted strategies. Each placement strategy is\n                randomly weighted against the rest. Attempts are made until all\n                strategies are exhausted. If none succeeds, the next group is\n                considered.\n                ',
                tunable=TunableTuple(
                    weight=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        The weight of this strategy relative to other strategies\n                        in its group.\n                        '
                    ),
                    placement_strategy=TunablePlacementStrategyVariant(
                        description=
                        '\n                        The placement strategy for the object in question.\n                        '
                    )),
                minlength=1),
            minlength=1)
    }

    def try_place_object(self, obj, resolver, **kwargs):
        for strategy_group in self.placement_strategy_groups:
            strategies = [(entry.weight.get_multiplier(resolver),
                           entry.placement_strategy)
                          for entry in strategy_group]
            while strategies:
                strategy = pop_weighted(strategies)
                if strategy.try_place_object(obj, resolver, **kwargs):
                    return True
        return False
Beispiel #5
0
class _ObjectDefinitionTested(CreationDataBase, HasTunableSingletonFactory,
                              AutoFactoryInit):
    FACTORY_TUNABLES = {
        'fallback_definition':
        TunableReference(
            description=
            '\n            Should no test pass, use this definition.\n            ',
            manager=services.definition_manager(),
            allow_none=True),
        'definitions':
        TunableList(
            description=
            '\n            A list of potential object definitions to use.\n            ',
            tunable=TunableTuple(
                weight=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    The weight of this definition relative to other\n                    definitions in this list.\n                    '
                ),
                definition=TunableReference(
                    description=
                    '\n                    The definition of the object to be created.\n                    ',
                    manager=services.definition_manager(),
                    pack_safe=True)))
    }

    def get_definition(self, resolver):
        definition = weighted_random_item([
            (pair.weight.get_multiplier(resolver), pair.definition)
            for pair in self.definitions
        ])
        if definition is not None:
            return definition
        return self.fallback_definition
class UpdateObjectValue(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'object_value_multipliers':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A list of test and multiplier pairs used for re-calculating (via\n            appraisal) the value of a portrait object.\n            '
        )
    }

    def _do_behavior(self):
        portrait_obj = self.interaction.target
        if portrait_obj is not None and portrait_obj.canvas_component is not None:
            sim_info = portrait_obj.get_component(
                STORED_SIM_INFO_COMPONENT).get_stored_sim_info()
            if sim_info is not None:
                portrait_obj.current_value = portrait_obj.catalog_value * self._get_total_multiplier(
                    sim_info)
                update_tooltip = portrait_obj.get_tooltip_field(
                    TooltipFieldsComplete.simoleon_value) is not None
                portrait_obj.update_current_value(update_tooltip)
                return True
        return False

    def _get_total_multiplier(self, sim_info):
        sim_resolver = SingleSimResolver(sim_info)
        multiplier = self.object_value_multipliers.get_multiplier(sim_resolver)
        return multiplier
 def get_create_params(user_facing=False):
     create_params = {}
     create_params_locked = {}
     if user_facing:
         create_params['user_facing'] = Tunable(
             description=
             "\n                                                   If enabled, we will start the situation as user facing.\n                                                   Note: We can only have one user facing situation at a time,\n                                                   so make sure you aren't tuning multiple user facing\n                                                   situations to occur at once.\n                                                   ",
             tunable_type=bool,
             default=False)
     else:
         create_params_locked['user_facing'] = False
     return {
         'weighted_situations':
         TunableList(
             description=
             '\n            A weighted list of situations to be used while fulfilling the\n            desired Sim count.\n            ',
             tunable=TunableTuple(
                 situation=Situation.TunableReference(pack_safe=True),
                 params=TunableTuple(
                     description=
                     '\n                    Situation creation parameters.\n                    ',
                     locked_args=create_params_locked,
                     **create_params),
                 weight=Tunable(tunable_type=int, default=1),
                 weight_multipliers=TunableMultiplier.TunableFactory(
                     description=
                     "\n                    Tunable tested multiplier to apply to weight.\n                    \n                    *IMPORTANT* The only participants that work are ones\n                    available globally, such as Lot and ActiveHousehold. Only\n                    use these participant types or use tests that don't rely\n                    on any, such as testing all objects via Object Criteria\n                    test or testing active zone with the Zone test.\n                    ",
                     locked_args={'base_value': 1}),
                 tests=TunableTestSet(
                     description=
                     "\n                    A set of tests that must pass for the situation and weight\n                    pair to be available for selection.\n                    \n                    *IMPORTANT* The only participants that work are ones\n                    available globally, such as Lot and ActiveHousehold. Only\n                    use these participant types or use tests that don't rely\n                    on any, such as testing all objects via Object Criteria\n                    test or testing active zone with the Zone test.\n                    "
                 )))
     }
Beispiel #8
0
class _RandomWeightedRecipe(_RandomRecipeBase, CreationDataBase,
                            HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'weighted_recipes':
        TunableList(
            description=
            '\n            A list of weighted list of recipes that can be available for recipe creation.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The weighted recipe.\n                ',
                recipe=_RecipeDefinition.TunableFactory(
                    recipe_factory_tuning={'pack_safe': True}),
                weight=TunableMultiplier.TunableFactory()),
            minlength=1)
    }

    def get_creation_params(self, resolver):
        weighted_recipe_creation_data = list(
            (weighted_recipe.weight.get_multiplier(resolver),
             weighted_recipe.recipe)
            for weighted_recipe in self.weighted_recipes)
        recipe_factory = weighted_random_item(weighted_recipe_creation_data)
        return ObjectCreationParams(
            recipe_factory.get_definition(resolver),
            {'chosen_creation_data': (recipe_factory, recipe_factory.recipe)})
 def get_create_params(**kwargs):
     return {
         '_desired_siutations':
         _DesiredSituations.TunableFactory(get_create_params=kwargs),
         'desired_sim_count_multipliers':
         TunableMultiplier.TunableFactory(
             description=
             '\n                Tunable tested multiplier to apply to the desired sim count.\n                ',
             locked_args={'base_value': 1})
     }
class Payment(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'payment_cost':
        TunableVariant(
            description=
            '\n            The type of payment, which defines the payment amount.\n            ',
            amount=PaymentAmount.TunableFactory(),
            amount_up_to=PaymentAmountUpTo.TunableFactory(),
            bills=PaymentBills.TunableFactory(),
            catalog_value=PaymentCatalogValue.TunableFactory(),
            current_value=PaymentCurrentValue.TunableFactory(),
            base_retail_value=PaymentBaseRetailValue.TunableFactory(),
            dining_meal_cost=PaymentBaseDiningBill.TunableFactory(),
            business_amount=PaymentBusinessAmount.TunableFactory(),
            input_dialog=PaymentDialog.TunableFactory(),
            liability=PaymentFromLiability.TunableFactory(),
            utility=PaymentUtility.TunableFactory(),
            default='amount'),
        'payment_source':
        get_tunable_payment_source_variant(
            description='\n            The source of the funds.\n            '
        ),
        'cost_modifiers':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of test sets and associated multipliers to apply to\n            the total cost of this payment.\n            '
        )
    }

    def get_simoleon_delta(self, resolver, override_amount=None):
        (amount, fund_source) = self.payment_cost.get_simoleon_delta(
            resolver, self.payment_source, self.cost_modifiers)
        if self.payment_source.require_full_amount:
            return (amount, fund_source)
        else:
            return (0, fund_source)

    def try_deduct_payment(self, resolver, sim, fail_callback=None):
        success = self.payment_cost.try_deduct_payment(resolver, sim,
                                                       fail_callback,
                                                       self.payment_source,
                                                       self.cost_modifiers)
        if not success and fail_callback:
            fail_callback()
        return success

    def get_cost_string(self):
        return self.payment_source.get_cost_string()

    def get_gain_string(self):
        return self.payment_source.get_gain_string()
class NeedEvaluation(EvaluationBase):
    FACTORY_TUNABLES = {
        'success_chance':
        TunableMultiplier.TunableFactory(
            description=
            '\n            Base success percentage and tested multipliers that evaluate to the\n            sum chance of earning the scholarship.\n            '
        )
    }

    def get_value(self, sim_info):
        pass

    def get_score(self, _, resolver, **kwargs):
        return self.success_chance.get_multiplier(resolver)
 def get_create_params(**kwargs):
     return {
         'entries':
         TunableList(
             description=
             '\n                A list of tuples declaring a relationship between days of the week\n                and desire curves.\n                ',
             tunable=TunableTuple(
                 description=
                 '\n                    The first value is the day of the week that maps to a desired\n                    curve of population by time of the day.\n                    \n                    days_of_the_week    population_desire_by_time_of_day\n                        M,Th,F                time_curve_1\n                        W,Sa                  time_curve_2\n                        \n                    By production/design request we do not support multiple\n                    population curves for the same day. e.g. if you want something\n                    special to occur at noon on a Wednesday, make a unique curve for\n                    Wednesday and apply the changes to it.\n                    ',
                 days_of_the_week=TunableDayAvailability(),
                 walkby_desire_by_time_of_day=TunableMapping(
                     description=
                     '\n                        Each entry in the map has two columns. The first column is\n                        the hour of the day (0-24) that maps to a desired list of\n                        population (second column).\n                        \n                        The entry with starting hour that is closest to, but before\n                        the current hour will be chosen.\n                        \n                        Given this tuning: \n                            hour_of_day           desired_situations\n                            6                     [(w1, s1), (w2, s2)]\n                            10                    [(w1, s2)]\n                            14                    [(w2, s5)]\n                            20                    [(w9, s0)]\n                            \n                        if the current hour is 11, hour_of_day will be 10 and desired is [(w1, s2)].\n                        if the current hour is 19, hour_of_day will be 14 and desired is [(w2, s5)].\n                        if the current hour is 23, hour_of_day will be 20 and desired is [(w9, s0)].\n                        if the current hour is 2, hour_of_day will be 20 and desired is [(w9, s0)]. (uses 20 tuning because it is not 6 yet)\n                        \n                        The entries will be automatically sorted by time.\n                        ',
                     key_name='hour_of_day',
                     key_type=Tunable(tunable_type=int, default=0),
                     value_name='desired_walkby_situations',
                     value_type=_DesiredSituations.TunableFactory(
                         get_create_params=kwargs)))),
         'desired_sim_count_multipliers':
         TunableMultiplier.TunableFactory(
             description=
             '\n                Tunable tested multiplier to apply to the desired sim count.\n                ',
             locked_args={'base_value': 1})
     }
Beispiel #13
0
class UniversityMajor(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR)):
    INSTANCE_TUNABLES = {'courses': TunableList(description='\n            List of courses, in order, for this major\n            ', tunable=UniversityCourseData.TunableReference(), minlength=1), 'acceptance_score': TunableTuple(description='\n            Score requirement to be accepted in this major as prestige degree.\n            ', score=TunableMultiplier.TunableFactory(description='\n                Define the base score and multiplier to calculate acceptance\n                score of a Sim.\n                '), score_threshold=TunableThreshold(description='\n                The threshold to perform against the score to see if a Sim \n                can be accepted in this major.\n                ')), 'display_name': TunableLocalizedString(description="\n            The major's name.\n            ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'display_description': TunableLocalizedString(description="\n            The major's description.\n            ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icons': TunableTuple(description='\n            Display icons for this major.\n            ', icon=TunableIcon(description="\n                The major's icon.\n                "), icon_prestige=TunableIcon(description="\n                The major's prestige icon.\n                "), icon_high_res=TunableIcon(description="\n                The major's high resolution icon.\n                "), icon_prestige_high_res=TunableIcon(description="\n                The major's prestige high resolution icon.\n                "), export_class_name='UniversityMajorIconTuple', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'major_benefit_map': TunableMapping(description='\n            University specific major benefit description. Each university can \n            have its own description defined for this University Major.\n            ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableLocalizedString(description='\n                Major benefit description.\n                '), tuple_name='UniversityMajorBenefitMapping', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'graduation_reward': TunableMapping(description='\n            Loot on graduation at each university for each GPA threshold\n            ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableList(description='\n                Loot for each GPA range (lower bound inclusive, upper bound\n                exclusive.\n                ', tunable=TunableTuple(gpa_range=TunableInterval(description='\n                        GPA range to receive this loot.\n                        Lower bound inclusive, upper bound exclusive.\n                        ', tunable_type=float, default_lower=0, default_upper=10), loot=TunableList(tunable=LootActions.TunableReference(description='\n                            The loot action applied.\n                            ', pack_safe=True))))), 'career_tracks': TunableList(description='\n            List of career tracks for which the UI will indicate this major\n            will provide benefit.  Is not used to actually provide said benefit.\n            ', tunable=TunableReference(description='\n                These are the career tracks that will benefit from this major.\n                ', manager=services.get_instance_manager(sims4.resources.Types.CAREER_TRACK), pack_safe=True), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary, unique_entries=True)}

    @classmethod
    def graduate(cls, sim_info, university, gpa):
        resolver = SingleSimResolver(sim_info)
        if university in cls.graduation_reward:
            for grad_reward in cls.graduation_reward[university]:
                if grad_reward.gpa_range.lower_bound <= gpa < grad_reward.gpa_range.upper_bound:
                    for loot_action in grad_reward.loot:
                        loot_action.apply_to_resolver(resolver)

    @classmethod
    def get_sim_acceptance_score(cls, sim_info):
        resolver = SingleSimResolver(sim_info)
        return cls.acceptance_score.score.get_multiplier(resolver)

    @classmethod
    def can_sim_be_accepted(cls, sim_info):
        sim_score = cls.get_sim_acceptance_score(sim_info)
        return cls.acceptance_score.score_threshold.compare(sim_score)
Beispiel #14
0
class University(HasTunableReference, _UniversityDisplayMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY)):
    COURSE_ELECTIVES = TunableTuple(description='\n        Tuning structure holding all electives data.\n        ', electives=TunableList(description='\n            A list of weighted elective courses that will be available in \n            all university.\n            ', tunable=TunableTuple(description='\n                Weighted elective course data.\n                ', elective=TunableReference(description='\n                    Elective course data.\n                    ', manager=services.get_instance_manager(Types.UNIVERSITY_COURSE_DATA), pack_safe=True), weight=TunableMultiplier.TunableFactory(description='\n                    The weight of this elective relative to other electives \n                    in this list.\n                    '))), elective_count=TunableInterval(description='\n            The number of elective courses to choose for enrollment from the \n            elective list. Random number will be chosen from the interval.\n            ', tunable_type=int, default_lower=8, default_upper=10, minimum=1, maximum=100), elective_change_frequency=TunableRange(description='\n            The frequency, in Sim days, at which the electives option will\n            regenerate.\n            ', tunable_type=int, default=1, minimum=1, maximum=50))
    ALL_UNIVERSITIES = TunableList(description='\n        A list of all available universities in the game.\n        ', tunable=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY), pack_safe=True), unique_entries=True)
    ALL_DEGREES = TunableList(description='\n        A list of all available degrees that will be available in all \n        university.\n        ', tunable=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR), pack_safe=True), unique_entries=True)
    SKILL_TO_MAJOR_TUNING = TunableMapping(description='\n        A mapping of Skill -> Majors that we can use to determine what the \n        appropriate major is for an existing Sim. Each Skill can be mapped to \n        a list of Majors. If more than one is specified then a random major \n        will be chosen if the Sim is being assigned a major based on that skill.\n        ', key_type=TunableReference(description='\n            The skill being used to assign the major.\n            ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',)), value_type=TunableList(description='\n            The set of majors to choose from when assigning a major based on \n            the associated skill type. If this has more than one entry then\n            one of the majors will be chosen at random.\n            ', tunable=TunableReference(description='\n                The university major to enroll the Sim in.\n                ', manager=services.get_instance_manager(sims4.resources.Types.UNIVERSITY_MAJOR), pack_safe=True), minlength=1, unique_entries=True))
    PROFESSOR_ARCHETYPES = TunableMapping(description='\n        A mapping of school to professor archetypes so that we can get\n        professors with the correct skill set for the college they will be\n        teaching at.\n        ', key_type=TunableReference(description='\n            The university that the professor archetype will belong to.\n            ', manager=services.get_instance_manager(sims4.resources.Types.UNIVERSITY), pack_safe=True), value_type=TunableList(description='\n            A list of Sim Filters used to find sims that match a certain archetype\n            and make them a professor by giving them the correct trait.\n            ', tunable=TunableReference(description='\n                A single Sim filter defining a professor archetype to search for.\n                \n                A search will be run using this filter as the archetype when\n                creating a professor and if no Sims match or can be conformed to\n                this filter then a new Sim will be created using the tuned Sim \n                Template.\n                ', manager=services.get_instance_manager(sims4.resources.Types.SIM_FILTER), pack_safe=True)))
    INSTANCE_TUNABLES = {'prestige_degrees': TunableList(description='\n            List of prestige degrees.\n            ', tunable=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR)), unique_entries=True, export_modes=ExportModes.All), 'organizations': TunableList(description='\n            List of organizations which are available in this university.\n            ', tunable=TunableReference(manager=services.get_instance_manager(Types.SNIPPET), class_restrictions=('Organization',)), unique_entries=True, export_modes=ExportModes.All), 'brochure_loot': TunableReference(description='\n            The loot to show university brochure.\n            ', manager=services.get_instance_manager(Types.ACTION), class_restrictions=('LootActions',)), 'mascot_label': Tunable(description='\n            Mascot label name to be used by enrollment wizard UI. \n            ', tunable_type=str, default='', export_modes=ExportModes.All, tuning_group=GroupNames.UI)}
    _all_degree_ids = None
    _prestige_degree_ids = None
    _non_prestige_degree_ids = None
    _non_prestige_degrees = None

    @classmethod
    def _verify_tuning_callback(cls):
        all_degrees = set(University.ALL_DEGREES)
        prestige_degrees = set(cls.prestige_degrees)
        if not prestige_degrees.issubset(all_degrees):
            logger.error('Prestige Degrees {} in University {} is not tuned as All Degrees in sims.university.university_tuning.', prestige_degrees - all_degrees, cls.__name__, owner='mkartika')

    @classproperty
    def all_degree_ids(cls):
        if cls._all_degree_ids is None:
            cls._all_degree_ids = [d.guid64 for d in cls.ALL_DEGREES]
        return cls._all_degree_ids

    @classproperty
    def prestige_degree_ids(cls):
        if cls._prestige_degree_ids is None:
            cls._prestige_degree_ids = [d.guid64 for d in cls.prestige_degrees]
        return cls._prestige_degree_ids

    @classproperty
    def non_prestige_degree_ids(cls):
        if cls._non_prestige_degree_ids is None:
            cls._non_prestige_degree_ids = [d.guid64 for d in cls.non_prestige_degrees]
        return cls._non_prestige_degree_ids

    @classproperty
    def non_prestige_degrees(cls):
        if cls._non_prestige_degrees is None:
            cls._non_prestige_degrees = [d for d in University.ALL_DEGREES if d not in cls.prestige_degrees]
        return cls._non_prestige_degrees

    @staticmethod
    def generate_elective_courses(resolver):
        elective_courses = []
        elective_count = random.randint(University.COURSE_ELECTIVES.elective_count.lower_bound, University.COURSE_ELECTIVES.elective_count.upper_bound)
        weighted_electives = []
        for e in University.COURSE_ELECTIVES.electives:
            if e.elective.course_skill_data.related_skill is None:
                continue
            weighted_electives.append((e.weight.get_multiplier(resolver), e.elective))
        for _ in range(elective_count):
            if not weighted_electives:
                break
            index = sims4.random.weighted_random_index(weighted_electives)
            if index is not None:
                weighted_elective = weighted_electives.pop(index)
                elective_courses.append(weighted_elective[1])
        return elective_courses

    @staticmethod
    def choose_random_university():
        if not University.ALL_UNIVERSITIES:
            return
        return random.choice(University.ALL_UNIVERSITIES)

    @staticmethod
    def choose_random_major():
        if not University.ALL_DEGREES:
            return
        return random.choice(University.ALL_DEGREES)
Beispiel #15
0
class Gig(HasTunableReference,
          _GigDisplayMixin,
          PrepTaskTrackerMixin,
          metaclass=HashedTunedInstanceMetaclass,
          manager=services.get_instance_manager(
              sims4.resources.Types.CAREER_GIG)):
    INSTANCE_TUNABLES = {
        'career':
        TunableReference(
            description=
            '\n            The career this gig is associated with.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.CAREER)),
        'gig_time':
        WeeklySchedule.TunableFactory(
            description=
            '\n            A tunable schedule that will determine when you have to be at work.\n            ',
            export_modes=ExportModes.All),
        'gig_prep_time':
        TunableTimeSpan(
            description=
            '\n            The amount of time between when a gig is selected and when it\n            occurs.\n            ',
            default_hours=5),
        'gig_prep_tasks':
        TunableList(
            description=
            '\n            A list of prep tasks the Sim can do to improve their performance\n            during the gig. \n            ',
            tunable=PrepTask.TunableFactory()),
        'loots_on_schedule':
        TunableList(
            description=
            '\n            Loot actions to apply when a sim gets a gig.\n            ',
            tunable=LootActions.TunableReference()),
        'audio_on_prep_task_completion':
        OptionalTunable(
            description=
            '\n            A sting to play at the time a prep task completes.\n            ',
            tunable=TunablePlayAudio(
                locked_args={
                    'immediate_audio': True,
                    'joint_name_hash': None,
                    'play_on_active_sim_only': True
                })),
        'gig_pay':
        TunableVariant(
            description=
            '\n            Base amount of pay for this gig. Can be either a flat amount or a\n            range.\n            ',
            range=TunableInterval(tunable_type=int,
                                  default_lower=0,
                                  default_upper=100,
                                  minimum=0),
            flat_amount=TunableIntervalLiteral(tunable_type=int,
                                               default=0,
                                               minimum=0),
            default='range'),
        'additional_pay_per_overmax_level':
        OptionalTunable(
            description=
            '\n            If checked, overmax levels will be considered when calculating pay\n            for this gig. The actual implementation of this may vary by gig\n            type.\n            ',
            tunable=TunableRange(tunable_type=int, default=0, minimum=0)),
        'result_based_gig_pay_multipliers':
        OptionalTunable(
            description=
            '\n            A set of multipliers for gig pay. The multiplier used depends on the\n            GigResult of the gig. The meanings of each GigResult may vary by\n            gig type.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the additional pay\n                the sim will receive.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=TunableMultiplier.TunableFactory())),
        'initial_result_based_career_performance':
        OptionalTunable(
            description=
            "\n            A mapping between the GigResult for this gig and the initial\n            career performance for the Sim's first gig.\n            ",
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the initial career\n                performance the Sim will receive.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=Tunable(
                    description=
                    '\n                    The initial performance value that will be applied.\n                    ',
                    tunable_type=float,
                    default=0))),
        'result_based_career_performance':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult for this gig and the change in\n            career performance for the sim.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the career\n                performance the sim will receive.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=Tunable(
                    description=
                    '\n                    The performance modification.\n                    ',
                    tunable_type=float,
                    default=0))),
        'result_based_career_performance_multiplier':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult and the multiplier for the career \n            performance awarded.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the career\n                performance multiplier.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    The performance modification multiplier.\n                    '
                ))),
        'result_based_loots':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult for this gig and a loot list to\n            optionally apply. The resolver for this loot list is either a\n            SingleSimResolver of the working sim or a DoubleSimResolver with the\n            target being the customer if there is a customer sim.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the loot list.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=TunableList(
                    description=
                    '\n                    Loot actions to apply.\n                    ',
                    tunable=LootActions.TunableReference(
                        description=
                        '\n                        The loot action applied.\n                        ',
                        pack_safe=True)))),
        'payout_stat_data':
        TunableMapping(
            description=
            '\n            Stats, and its associated information, that are gained (or lost) \n            when sim finishes this gig.\n            ',
            key_type=TunableReference(
                description=
                '\n                Stat for this payout.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.STATISTIC)),
            value_type=TunableTuple(
                description=
                '\n                Data about this payout stat. \n                ',
                base_amount=Tunable(
                    description=
                    '\n                    Base amount (pre-modifiers) applied to the sim at the end\n                    of the gig.\n                    ',
                    tunable_type=float,
                    default=0.0),
                medal_to_payout=TunableMapping(
                    description=
                    '\n                    Mapping of medal -> stat multiplier.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        Medal achieved in this gig.\n                        ',
                        tunable_type=SituationMedal,
                        default=SituationMedal.TIN),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        Mulitiplier on statistic payout if scorable situation\n                        ends with the associate medal.\n                        '
                    )),
                ui_threshold=TunableList(
                    description=
                    '\n                    Thresholds and icons we use for this stat to display in \n                    the end of day dialog. Tune in reverse of highest threshold \n                    to lowest threshold.\n                    ',
                    tunable=TunableTuple(
                        description=
                        '\n                        Threshold and icon for this stat and this gig.\n                        ',
                        threshold_icon=TunableIcon(
                            description=
                            '\n                            Icon if the stat is of this threshold.\n                            '
                        ),
                        threshold_description=TunableLocalizedStringFactory(
                            description=
                            '\n                            Description to use with icon\n                            '
                        ),
                        threshold=Tunable(
                            description=
                            '\n                            Threshold that the stat must >= to.\n                            ',
                            tunable_type=float,
                            default=0.0))))),
        'career_events':
        TunableList(
            description=
            '\n             A list of available career events for this gig.\n             ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.CAREER_EVENT))),
        'gig_cast_rel_bit_collection_id':
        TunableEnumEntry(
            description=
            '\n            If a rel bit is applied to the cast member, it must be of this collection id.\n            We use this to clear the rel bit when the gig is over.\n            ',
            tunable_type=RelationshipBitCollectionUid,
            default=RelationshipBitCollectionUid.Invalid,
            invalid_enums=(RelationshipBitCollectionUid.All, )),
        'gig_cast':
        TunableList(
            description=
            '\n            This is the list of sims that need to spawn for this gig. \n            ',
            tunable=TunableTuple(
                description=
                '\n                Data for cast members. It contains a test which tests against \n                the owner of this gig and spawn the necessary sims. A bit\n                may be applied through the loot action to determine the type \n                of cast members (costars, directors, etc...) \n                ',
                filter_test=TunableTestSet(
                    description=
                    '\n                    Test used on owner sim.\n                    '
                ),
                sim_filter=TunableSimFilter.TunableReference(
                    description=
                    '\n                    If filter test is passed, this sim is created and stored.\n                    '
                ),
                cast_member_rel_bit=OptionalTunable(
                    description=
                    '\n                    If tuned, this rel bit will be applied on the spawned cast \n                    member.\n                    ',
                    tunable=RelationshipBit.TunableReference(
                        description=
                        '\n                        Rel bit to apply.\n                        '
                    )))),
        'end_of_gig_dialog':
        OptionalTunable(
            description=
            '\n            A results dialog to show. This dialog allows a list\n            of icons with labels. Stats are added at the end of this icons.\n            ',
            tunable=UiDialogLabeledIcons.TunableFactory()),
        'disabled_tooltip':
        OptionalTunable(
            description=
            '\n            If tuned, the tooltip when this row is disabled.\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_notifications':
        OptionalTunable(
            description=
            '\n            If enabled, a notification to show at the end of the gig instead of\n            a normal career message. Tokens are:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The pay for the gig\n            * 5: The gratuity for the gig\n            * 6: The customer (sim) of the gig, if there is a customer.\n            * 7: A bullet list of loots and payments as a result of this gig.\n                 This list uses the text tuned on the loots themselves to create\n                 bullets for each loot. Those texts will generally have tokens 0\n                 and 1 be the subject and target sims (of the loot) but may\n                 have additional tokens depending on the type of loot.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the post-gig\n                notification.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=_get_career_notification_tunable_factory()),
            tuning_group=GroupNames.UI),
        'end_of_gig_overmax_notification':
        OptionalTunable(
            description=
            '\n            If tuned, the notification that will be used if the sim gains an\n            overmax level during this gig. Will override the overmax\n            notification in career messages. The following tokens are provided:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The overmax level\n            * 5: The pay for the gig\n            * 6: Additional pay tuned at additional_pay_per_overmax_level \n            * 7: The overmax rewards in a bullet-point list, in the form of a\n                 string. These are tuned on the career_track\n            ',
            tunable=_get_career_notification_tunable_factory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_overmax_rewardless_notification':
        OptionalTunable(
            description=
            '\n            If tuned, the notification that will be used if the sim gains an\n            overmax level with no reward during this gig. Will override the\n            overmax rewardless notification in career messages.The following\n            tokens are provided:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The overmax level\n            * 5: The pay for the gig\n            * 6: Additional pay tuned at additional_pay_per_overmax_level \n            ',
            tunable=_get_career_notification_tunable_factory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_promotion_text':
        OptionalTunable(
            description=
            '\n            A string that, if enabled, will be pre-pended to the bullet\n            list of results in the promotion notification. Tokens are:\n            * 0 : The Sim owner of the career\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_demotion_text':
        OptionalTunable(
            description=
            '\n            A string that, if enabled, will be pre-pended to the bullet\n            list of results in the promotion notification. Tokens are:\n            * 0 : The Sim owner of the career\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'odd_job_tuning':
        OptionalTunable(
            description=
            '\n            Tuning specific to odd jobs. Leave untuned if this gig is not an\n            odd job.\n            ',
            tunable=TunableTuple(
                customer_description=TunableLocalizedStringFactory(
                    description=
                    '\n                    The description of the odd job written by the customer.\n                    Token 0 is the customer sim.\n                    '
                ),
                use_customer_description_as_gig_description=Tunable(
                    description=
                    '\n                    If checked, the customer description will be used as the\n                    gig description. This description is used as the tooltip\n                    for the gig icon in the career panel.\n                    ',
                    tunable_type=bool,
                    default=False),
                result_based_gig_gratuity_multipliers=TunableMapping(
                    description=
                    '\n                    A set of multipliers for the gig gratuity.  This maps the\n                    result type of the gig and the gratuity multiplier (a \n                    percentage).  The base pay will be multiplied by this \n                    multiplier in order to determine the actual gratuity \n                    amount.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        The GigResult enum that represents the outcome of the \n                        Gig.\n                        ',
                        tunable_type=GigResult,
                        default=GigResult.SUCCESS),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        The gratuity multiplier to be calculated for this \n                        GigResult.\n                        '
                    )),
                result_based_gig_gratuity_chance_multipliers=TunableMapping(
                    description=
                    '\n                    A set of multipliers for determining the gig gratuity \n                    chance (i.e., the probability the Sim will receive gratuity \n                    in addition to the base pay).  The gratuity chance depends \n                    on the GigResult of the gig.  This maps the result type of \n                    the gig and the gratuity chance/percentage.  If this map\n                    (or a GigResult) is left untuned, then no gratuity is \n                    added.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        The GigResult enum that represents the outcome of the \n                        Gig.\n                        ',
                        tunable_type=GigResult,
                        default=GigResult.SUCCESS),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        The multiplier to be calculated for this GigResult.  \n                        This represents the percentage chance the Sim will \n                        receive gratuity.  If the Sim is to not receive \n                        gratuity, the base value should be 0 (without further\n                        tests).  If this Sim is guaranteed to receive gratuity,\n                        the base value should be 1 (without further tests).\n                        '
                    )),
                gig_gratuity_bullet_point_text=OptionalTunable(
                    description=
                    '\n                    If enabled, the gig gratuity will be a bullet point in the\n                    bullet pointed list of loots and money supplied to the end\n                    of gig notification (this is token 7 of that notification).\n                    If disabled, gratuity will be omitted from that list.\n                    Tokens:\n                    * 0: The sim owner of the career\n                    * 1: The customer\n                    * 2: the gratuity amount \n                    ',
                    tunable=TunableLocalizedStringFactory()))),
        'tip':
        OptionalTunable(
            description=
            '\n            A tip that is displayed with the gig in pickers and in the career\n            panel. Can produce something like "Required Skill: Fitness 2".\n            ',
            tunable=TunableTuple(
                tip_title=TunableLocalizedStringFactory(
                    description=
                    '\n                    The title string of the tip. Could be something like "Required\n                    Skill.\n                    '
                ),
                tip_text=TunableLocalizedStringFactory(
                    description=
                    '\n                    The text string of the tip. Could be something like "Fitness 2".\n                    '
                ),
                tip_icon=OptionalTunable(tunable=TunableIcon(
                    description=
                    '\n                        An icon to show along with the tip.\n                        '
                )))),
        'critical_failure_test':
        OptionalTunable(
            description=
            '\n            The tests for checking whether or not the Sim should receive the \n            CRITICAL_FAILURE outcome.  This will override other GigResult \n            behavior.\n            ',
            tunable=TunableTestSet(
                description=
                '\n                The tests to be performed on the Sim (and any customer).  If \n                the tests pass, the outcome will be CRITICAL_FAILURE.  \n                '
            ))
    }

    def __init__(self, owner, customer=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner
        self._customer_id = customer.id if customer is not None else None
        self._upcoming_gig_time = None
        self._gig_result = None
        self._gig_pay = None
        self._gig_gratuity = None
        self._loot_strings = None
        self._gig_attended = False

    @classmethod
    def get_aspiration(cls):
        pass

    @classmethod
    def get_time_until_next_possible_gig(cls, starting_time):
        required_prep_time = cls.gig_prep_time()
        start_considering_prep = starting_time + required_prep_time
        (time_until, _) = cls.gig_time().time_until_next_scheduled_event(
            start_considering_prep)
        if not time_until:
            return
        return time_until + required_prep_time

    def register_aspiration_callbacks(self):
        aspiration = self.get_aspiration()
        if aspiration is None:
            return
        aspiration.register_callbacks()
        aspiration_tracker = self._owner.aspiration_tracker
        if aspiration_tracker is None:
            return
        aspiration_tracker.validate_and_return_completed_status(aspiration)
        aspiration_tracker.process_test_events_for_aspiration(aspiration)

    def notify_gig_attended(self):
        self._gig_attended = True

    def has_attended_gig(self):
        return self._gig_attended

    def notify_canceled(self):
        self._gig_result = GigResult.CANCELED
        self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_CANCEL)

    def get_career_performance(self, first_gig=False):
        if not self.result_based_career_performance:
            return 0
        if self.initial_result_based_career_performance is not None and first_gig and self._gig_result in self.initial_result_based_career_performance:
            return self.initial_result_based_career_performance[
                self._gig_result]
        performance_modifier = 1
        if self.result_based_career_performance_multiplier:
            if self._gig_result in self.result_based_career_performance_multiplier:
                resolver = self.get_resolver_for_gig()
                performance_modifier = self.result_based_career_performance_multiplier[
                    self._gig_result].get_multiplier(resolver)
        return self.result_based_career_performance.get(
            self._gig_result, 0) * performance_modifier

    def treat_work_time_as_due_date(self):
        return False

    @classmethod
    def create_picker_row(cls,
                          description=None,
                          scheduled_time=None,
                          owner=None,
                          gig_customer=None,
                          enabled=True,
                          **kwargs):
        tip = cls.tip
        description = cls.gig_picker_localization_format(
            cls.gig_pay.lower_bound, cls.gig_pay.upper_bound, scheduled_time,
            tip.tip_title(), tip.tip_text(), gig_customer)
        if not enabled and cls.disabled_tooltip is not None:
            row_tooltip = lambda *_: cls.disabled_tooltip(owner)
        elif cls.display_description is None:
            row_tooltip = None
        else:
            row_tooltip = lambda *_: cls.display_description(owner)
        if cls.odd_job_tuning is not None:
            customer_description = cls.odd_job_tuning.customer_description(
                gig_customer)
            row = OddJobPickerRow(customer_id=gig_customer.id,
                                  customer_description=customer_description,
                                  tip_title=tip.tip_title(),
                                  tip_text=tip.tip_text(),
                                  tip_icon=tip.tip_icon,
                                  name=cls.display_name(owner),
                                  icon=cls.display_icon,
                                  row_description=description,
                                  row_tooltip=row_tooltip,
                                  is_enable=enabled)
        else:
            row = ObjectPickerRow(name=cls.display_name(owner),
                                  icon=cls.display_icon,
                                  row_description=description,
                                  row_tooltip=row_tooltip,
                                  is_enable=enabled)
        return row

    def get_gig_time(self):
        return self._upcoming_gig_time

    def get_gig_customer(self):
        return self._customer_id

    def clean_up_gig(self):
        if self.gig_prep_tasks:
            self.prep_task_cleanup()

    def save_gig(self, gig_proto_buff):
        gig_proto_buff.gig_type = self.guid64
        gig_proto_buff.gig_time = self._upcoming_gig_time
        if hasattr(gig_proto_buff, 'gig_attended'):
            gig_proto_buff.gig_attended = self._gig_attended
        if self._customer_id:
            gig_proto_buff.customer_sim_id = self._customer_id

    def load_gig(self, gig_proto_buff):
        self._upcoming_gig_time = DateAndTime(gig_proto_buff.gig_time)
        if self.gig_prep_tasks:
            self.prep_time_start(self._owner,
                                 self.gig_prep_tasks,
                                 self.guid64,
                                 self.audio_on_prep_task_completion,
                                 from_load=True)
        if gig_proto_buff.HasField('customer_sim_id'):
            self._customer_id = gig_proto_buff.customer_sim_id
        if gig_proto_buff.HasField('gig_attended'):
            self._gig_attended = gig_proto_buff.gig_attended

    def set_gig_time(self, upcoming_gig_time):
        self._upcoming_gig_time = upcoming_gig_time

    def get_resolver_for_gig(self):
        if self._customer_id is not None:
            customer_sim_info = services.sim_info_manager().get(
                self._customer_id)
            if customer_sim_info is not None:
                return DoubleSimResolver(self._owner, customer_sim_info)
        return SingleSimResolver(self._owner)

    def set_up_gig(self):
        if self.gig_prep_tasks:
            self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64,
                                 self.audio_on_prep_task_completion)
        if self.loots_on_schedule:
            resolver = self.get_resolver_for_gig()
            for loot_actions in self.loots_on_schedule:
                loot_actions.apply_to_resolver(resolver)
        self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_STARTED)

    def collect_rabbit_hole_rewards(self):
        pass

    def _get_additional_loots(self):
        if self.result_based_loots is not None and self._gig_result is not None:
            loots = self.result_based_loots.get(self._gig_result)
            if loots is not None:
                return loots
        return ()

    def collect_additional_rewards(self):
        loots = self._get_additional_loots()
        if loots:
            self._loot_strings = []
            resolver = self.get_resolver_for_gig()
            for loot_actions in loots:
                self._loot_strings.extend(
                    loot_actions.apply_to_resolver_and_get_display_texts(
                        resolver))

    def _determine_gig_outcome(self):
        raise NotImplementedError

    def get_pay(self, overmax_level=None, **kwargs):
        self._determine_gig_outcome()
        pay = self.gig_pay.lower_bound
        if self.additional_pay_per_overmax_level:
            pay = pay + overmax_level * self.additional_pay_per_overmax_level
        resolver = self.get_resolver_for_gig()
        if self.result_based_gig_pay_multipliers:
            if self._gig_result in self.result_based_gig_pay_multipliers:
                multiplier = self.result_based_gig_pay_multipliers[
                    self._gig_result].get_multiplier(resolver)
                pay = int(pay * multiplier)
        gratuity = 0
        if self.odd_job_tuning:
            if self.odd_job_tuning.result_based_gig_gratuity_multipliers:
                if self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers:
                    gratuity_multiplier = 0
                    gratuity_chance = 0
                    if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers:
                        gratuity_chance = self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers[
                            self._gig_result].get_multiplier(resolver)
                    if random.random() <= gratuity_chance:
                        if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_multipliers:
                            gratuity_multiplier = self.odd_job_tuning.result_based_gig_gratuity_multipliers[
                                self._gig_result].get_multiplier(resolver)
                            gratuity = int(pay * gratuity_multiplier)
        self._gig_pay = pay
        self._gig_gratuity = gratuity
        return pay + gratuity

    def get_promotion_evaluation_result(self,
                                        reward_text,
                                        *args,
                                        first_gig=False,
                                        **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                if self.end_of_gig_promotion_text and not first_gig:
                    results_list = self.get_results_list(
                        self.end_of_gig_promotion_text(self._owner))
                else:
                    results_list = self.get_results_list()
                return EvaluationResult(Evaluation.PROMOTED, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info, results_list)

    def get_demotion_evaluation_result(self, *args, first_gig=False, **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                if self.end_of_gig_demotion_text and not first_gig:
                    results_list = self.get_results_list(
                        self.end_of_gig_demotion_text(self._owner))
                else:
                    results_list = self.get_results_list()
                return EvaluationResult(Evaluation.DEMOTED, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info, results_list)

    def get_overmax_evaluation_result(self, overmax_level, reward_text, *args,
                                      **kwargs):
        if reward_text and self.end_of_gig_overmax_notification:
            return EvaluationResult(Evaluation.PROMOTED,
                                    self.end_of_gig_overmax_notification,
                                    overmax_level, self._gig_pay,
                                    self.additional_pay_per_overmax_level,
                                    reward_text)
        elif self.end_of_gig_overmax_rewardless_notification:
            return EvaluationResult(
                Evaluation.PROMOTED,
                self.end_of_gig_overmax_rewardless_notification, overmax_level,
                self._gig_pay, self.additional_pay_per_overmax_level)

    def _get_strings_for_results_list(self):
        strings = []
        if self._gig_pay is not None:
            strings.append(LocalizationHelperTuning.MONEY(self._gig_pay))
        if self.odd_job_tuning is not None and self._gig_gratuity:
            gratuity_text_factory = self.odd_job_tuning.gig_gratuity_bullet_point_text
            if gratuity_text_factory is not None:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                gratuity_text = gratuity_text_factory(self._owner,
                                                      customer_sim_info,
                                                      self._gig_gratuity)
                strings.append(gratuity_text)
        if self._loot_strings:
            strings.extend(self._loot_strings)
        return strings

    def get_results_list(self, *additional_tokens):
        return LocalizationHelperTuning.get_bulleted_list(
            None, *additional_tokens, *self._get_strings_for_results_list())

    def get_end_of_gig_evaluation_result(self, **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                return EvaluationResult(Evaluation.ON_TARGET, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info,
                                        self.get_results_list())

    @classmethod
    def _get_base_pay_for_gig_owner(cls, owner):
        overmax_pay = cls.additional_pay_per_overmax_level
        if overmax_pay is not None:
            career = owner.career_tracker.get_career_by_uid(cls.career.guid64)
            if career is not None:
                overmax_pay *= career.overmax_level
                return (cls.gig_pay.lower_bound + overmax_pay,
                        cls.gig_pay.upper_bound + overmax_pay)
        return (cls.gig_pay.lower_bound, cls.gig_pay.upper_bound)

    @classmethod
    def build_gig_msg(cls, msg, sim, gig_time=None, gig_customer=None):
        msg.gig_type = cls.guid64
        msg.gig_name = cls.display_name(sim)
        (pay_lower, pay_upper) = cls._get_base_pay_for_gig_owner(sim)
        msg.min_pay = pay_lower
        msg.max_pay = pay_upper
        msg.gig_icon = ResourceKey()
        msg.gig_icon.instance = cls.display_icon.instance
        msg.gig_icon.group = cls.display_icon.group
        msg.gig_icon.type = cls.display_icon.type
        if cls.odd_job_tuning is not None and cls.odd_job_tuning.use_customer_description_as_gig_description and gig_customer is not None:
            customer_sim_info = services.sim_info_manager().get(gig_customer)
            if customer_sim_info is not None:
                msg.gig_description = cls.odd_job_tuning.customer_description(
                    customer_sim_info)
        else:
            msg.gig_description = cls.display_description(sim)
        if gig_time is not None:
            msg.gig_time = gig_time
        if gig_customer is not None:
            msg.customer_id = gig_customer
        if cls.tip is not None:
            msg.tip_title = cls.tip.tip_title()
            if cls.tip.tip_icon is not None or cls.tip.tip_text is not None:
                build_icon_info_msg(
                    IconInfoData(icon_resource=cls.tip.tip_icon),
                    None,
                    msg.tip_icon,
                    desc=cls.tip.tip_text())

    def send_prep_task_update(self):
        if self.gig_prep_tasks:
            self._prep_task_tracker.send_prep_task_update()

    def _apply_payout_stat(self, medal, payout_display_data=None):
        owner_sim = self._owner
        resolver = SingleSimResolver(owner_sim)
        payout_stats = self.payout_stat_data
        for stat in payout_stats.keys():
            stat_tracker = owner_sim.get_tracker(stat)
            if not owner_sim.get_tracker(stat).has_statistic(stat):
                continue
            stat_data = payout_stats[stat]
            stat_multiplier = 1.0
            if medal in stat_data.medal_to_payout:
                multiplier = stat_data.medal_to_payout[medal]
                stat_multiplier = multiplier.get_multiplier(resolver)
            stat_total = stat_data.base_amount * stat_multiplier
            stat_tracker.add_value(stat, stat_total)
            if payout_display_data is not None:
                for threshold_data in stat_data.ui_threshold:
                    if stat_total >= threshold_data.threshold:
                        payout_display_data.append(threshold_data)
                        break

    def _send_gig_telemetry(self, progress):
        with telemetry_helper.begin_hook(gig_telemetry_writer,
                                         TELEMETRY_HOOK_GIG_PROGRESS,
                                         sim_info=self._owner) as hook:
            hook.write_int(TELEMETRY_CAREER_ID, self.career.guid64)
            hook.write_int(TELEMETRY_GIG_ID, self.guid64)
            hook.write_int(TELEMETRY_GIG_PROGRESS_NUMBER, progress)
class AuditionDramaNode(BaseDramaNode):
    INSTANCE_TUNABLES = {
        'gig':
        Gig.TunableReference(
            description='\n            Gig this audition is for.\n            '
        ),
        'audition_prep_time':
        TunableTimeSpan(
            description=
            '\n            Amount of time between the seed of the potential audition node\n            to the start of the audition time. \n            ',
            default_hours=5),
        'audition_prep_recommendation':
        TunableLocalizedStringFactory(
            description=
            '\n            String that gives the player more information on how to succeed\n            in this audition.\n            '
        ),
        'audition_prep_icon':
        OptionalTunable(
            description=
            '\n            If enabled, this icon will be displayed with the audition preparation.\n            ',
            tunable=TunableIcon(
                description=
                '\n                Icon for audition preparation.\n                '
            )),
        'audition_outcomes':
        TunableList(
            description=
            '\n            List of loot and multipliers which are for audition outcomes.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The information needed to determine whether or not the sim passes\n                or fails this audition. We cannot rely on the outcome of the \n                interaction because we need to run this test on uninstantiated \n                sims as well. This is similar to the fallback outcomes in \n                interactions.\n                ',
                loot_list=TunableList(
                    description=
                    '\n                    Loot applied if this outcome is chosen\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)),
                weight=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    A tunable list of tests and multipliers to apply to the \n                    weight of the outcome.\n                    '
                ),
                is_success=Tunable(
                    description=
                    '\n                    Whether or not this is considered a success outcome.\n                    ',
                    tunable_type=bool,
                    default=False))),
        'audition_rabbit_hole':
        RabbitHole.TunableReference(
            description=
            '\n            Data required to put sim in rabbit hole.\n            '
        ),
        'skip_audition':
        OptionalTunable(
            description=
            '\n            If enabled, we can skip auditions if sim passes tuned tests.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data related to whether or not this audition can be skipped.\n                ',
                skip_audition_tests=TunableTestSet(
                    description=
                    '\n                    Test to see if sim can skip this audition.\n                    '
                ),
                skipped_audition_loot=TunableList(
                    description=
                    '\n                    Loot applied if sim manages to skip audition\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)))),
        'advance_notice_time':
        TunableTimeSpan(
            description=
            '\n            The amount of time between the alert and the start of the event.\n            ',
            default_hours=1,
            locked_args={
                'days': 0,
                'minutes': 0
            }),
        'loot_on_schedule':
        TunableList(
            description=
            '\n            Loot applied if the audition drama node is scheduled successfully.\n            ',
            tunable=LootActions.TunableReference(pack_safe=True)),
        'advance_notice_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that is displayed at the advance notice time.\n            '
        )
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._calculated_audition_time = None
        self._calculated_gig_time = None
        self._rabbit_hole_id = None

    @classproperty
    def drama_node_type(cls):
        return DramaNodeType.AUDITION

    @property
    def _require_instanced_sim(self):
        return False

    @classproperty
    def persist_when_active(cls):
        return True

    def get_picker_schedule_time(self):
        return self._calculated_audition_time

    def create_picker_row(self, owner=None, **kwargs):
        now_time = services.game_clock_service().now()
        min_audition_time = now_time + self.audition_prep_time()
        possible_audition_times = self.get_final_times_based_on_schedule(
            self.min_and_max_times,
            anchor_time=min_audition_time,
            scheduled_time_only=True)
        audition_time = min_audition_time
        if possible_audition_times is not None:
            now = services.time_service().sim_now
            for possible_audition_time in possible_audition_times:
                if possible_audition_time[0] >= now:
                    audition_time = possible_audition_time[0]
                    break
        gig = self.gig
        time_till_gig = gig.get_time_until_next_possible_gig(audition_time)
        if time_till_gig is None:
            return
        gig_time = audition_time + time_till_gig
        if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests(
                SingleSimResolver(owner)):
            formatted_string = Career.GIG_PICKER_SKIPPED_AUDITION_LOCALIZATION_FORMAT(
                gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, gig_time,
                self.audition_prep_recommendation())
        else:
            formatted_string = Career.GIG_PICKER_LOCALIZATION_FORMAT(
                gig.gig_pay.lower_bound, gig.gig_pay.upper_bound,
                audition_time, gig_time, self.audition_prep_recommendation())
        self._calculated_audition_time = audition_time
        self._calculated_gig_time = gig_time
        return gig.create_picker_row(formatted_string, owner)

    def schedule(self,
                 resolver,
                 specific_time=None,
                 time_modifier=TimeSpan.ZERO):
        if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests(
                resolver):
            for loot in self.skip_audition.skipped_audition_loot:
                loot.apply_to_resolver(resolver)
            resolver.sim_info_to_test.career_tracker.set_gig(
                self.gig, self._calculated_gig_time)
            return False
        success = super().schedule(resolver,
                                   specific_time=specific_time,
                                   time_modifier=time_modifier)
        if success:
            services.calendar_service().mark_on_calendar(
                self, advance_notice_time=self.advance_notice_time())
            self._send_career_ui_update(is_add=True)
            for loot in self.loot_on_schedule:
                loot.apply_to_resolver(resolver)
        return success

    def cleanup(self, from_service_stop=False):
        services.calendar_service().remove_on_calendar(self.uid)
        self._send_career_ui_update(is_add=False)
        rabbit_hole_service = services.get_rabbit_hole_service()
        if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole(
                self._receiver_sim_info.id,
                rabbit_hole_id=self._rabbit_hole_id):
            rabbit_hole_service.remove_rabbit_hole_expiration_callback(
                self._receiver_sim_info.id, self._rabbit_hole_id,
                self._on_sim_return)
        super().cleanup(from_service_stop=from_service_stop)

    def resume(self):
        if self._rabbit_hole_id and not services.get_rabbit_hole_service(
        ).is_in_rabbit_hole(self._receiver_sim_info.id,
                            rabbit_hole_id=self._rabbit_hole_id):
            services.drama_scheduler_service().complete_node(self.uid)

    def _run(self):
        rabbit_hole_service = services.get_rabbit_hole_service()
        self._rabbit_hole_id = rabbit_hole_service.put_sim_in_managed_rabbithole(
            self._receiver_sim_info, self.audition_rabbit_hole)
        if self._rabbit_hole_id is None:
            self._on_sim_return(canceled=True)
        rabbit_hole_service.set_rabbit_hole_expiration_callback(
            self._receiver_sim_info.id, self._rabbit_hole_id,
            self._on_sim_return)
        return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE

    def _on_sim_return(self, canceled=False):
        receiver_sim_info = self._receiver_sim_info
        resolver = SingleSimResolver(receiver_sim_info)
        weights = []
        failure_outcomes = []
        for outcome in self.audition_outcomes:
            if canceled:
                if not outcome.is_success:
                    failure_outcomes.append(outcome)
                    weight = outcome.weight.get_multiplier(resolver)
                    if weight > 0:
                        weights.append((weight, outcome))
            else:
                weight = outcome.weight.get_multiplier(resolver)
                if weight > 0:
                    weights.append((weight, outcome))
        if failure_outcomes:
            selected_outcome = random.choice(failure_outcomes)
        else:
            selected_outcome = sims4.random.weighted_random_item(weights)
        if not selected_outcome:
            logger.error(
                'No valid outcome is tuned on this audition. Verify weights in audition_outcome for {}.',
                self.guid64)
            services.drama_scheduler_service().complete_node(self.uid)
            return
        if selected_outcome.is_success:
            receiver_sim_info.career_tracker.set_gig(self.gig,
                                                     self._calculated_gig_time)
        for loot in selected_outcome.loot_list:
            loot.apply_to_resolver(resolver)
        services.drama_scheduler_service().complete_node(self.uid)

    def _save_custom_data(self, writer):
        if self._calculated_audition_time is not None:
            writer.write_uint64(AUDITION_TIME_TOKEN,
                                self._calculated_audition_time)
        if self._calculated_gig_time is not None:
            writer.write_uint64(GIG_TIME_TOKEN, self._calculated_gig_time)
        if self._rabbit_hole_id is not None:
            writer.write_uint64(RABBIT_HOLE_ID_TOKEN, self._rabbit_hole_id)

    def _load_custom_data(self, reader):
        self._calculated_audition_time = DateAndTime(
            reader.read_uint64(AUDITION_TIME_TOKEN, None))
        self._calculated_gig_time = DateAndTime(
            reader.read_uint64(GIG_TIME_TOKEN, None))
        self._rabbit_hole_id = reader.read_uint64(RABBIT_HOLE_ID_TOKEN, None)
        rabbit_hole_service = services.get_rabbit_hole_service()
        if not self._rabbit_hole_id:
            rabbit_hole_service = services.get_rabbit_hole_service()
            self._rabbit_hole_id = services.get_rabbit_hole_service(
            ).get_rabbit_hole_id_by_type(self._receiver_sim_info.id,
                                         self.audition_rabbit_hole)
        if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole(
                self._receiver_sim_info.id,
                rabbit_hole_id=self._rabbit_hole_id):
            rabbit_hole_service.set_rabbit_hole_expiration_callback(
                self._receiver_sim_info.id, self._rabbit_hole_id,
                self._on_sim_return)
        self._send_career_ui_update()
        return True

    def _send_career_ui_update(self, is_add=True):
        audition_update_msg = DistributorOps_pb2.AuditionUpdate()
        if is_add:
            self.gig.build_gig_msg(
                audition_update_msg.audition_info,
                self._receiver_sim_info,
                gig_time=self._calculated_gig_time,
                audition_time=self._calculated_audition_time)
        op = GenericProtocolBufferOp(Operation.AUDITION_UPDATE,
                                     audition_update_msg)
        build_icon_info_msg(
            IconInfoData(icon_resource=self.audition_prep_icon),
            self.audition_prep_recommendation(),
            audition_update_msg.recommended_task)
        Distributor.instance().add_op(self._receiver_sim_info, op)

    def load(self, drama_node_proto, schedule_alarm=True):
        super_success = super().load(drama_node_proto,
                                     schedule_alarm=schedule_alarm)
        if not super_success:
            return False
        services.calendar_service().mark_on_calendar(
            self, advance_notice_time=self.advance_notice_time())
        return True

    def on_calendar_alert_alarm(self):
        receiver_sim_info = self._receiver_sim_info
        resolver = SingleSimResolver(receiver_sim_info)
        dialog = self.advance_notice_notification(receiver_sim_info,
                                                  resolver=resolver)
        dialog.show_dialog()
Beispiel #17
0
class LifestyleBrandTracker(SimInfoTracker):
    PAYMENT_SCHEDULE = WeeklySchedule.TunableFactory(
        description=
        '\n        The schedule for when payments should be made. This is global to all\n        Sims that have lifestyle brands.\n        '
    )
    LIFESTYLE_BRAND_PAYOUT_MAPPING = TunableMapping(
        description=
        "\n        This is a mapping of target market to another mapping of product to\n        payout curve.\n        \n        Each combination of target market and product will have it's own unique\n        payout curve.\n        ",
        key_type=TunableEnumEntry(
            description=
            '\n            An enum representation of the different kinds of target markets \n            that are available as options for the lifestyle brand.\n            ',
            tunable_type=LifestyleBrandTargetMarket,
            default=LifestyleBrandTargetMarket.INVALID,
            invalid_enums=(LifestyleBrandTargetMarket.INVALID, )),
        value_type=TunableMapping(
            description=
            '\n            This mapping is of product to payout curve.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                An enum representation of the products that are available as \n                options for the lifestyle brand.\n                ',
                tunable_type=LifestyleBrandProduct,
                default=LifestyleBrandProduct.INVALID,
                invalid_enums=(LifestyleBrandProduct.INVALID, )),
            value_type=TunableTuple(
                description=
                '\n                Data required to calculate the payout for this product.\n                ',
                curve=TunableCurve(
                    description=
                    '\n                    This curve is days to payout amount in simoleons.\n                    ',
                    x_axis_name='Days Active',
                    y_axis_name='Simoleon Amount'),
                payment_deviation_percent=Tunable(
                    description=
                    '\n                    Once the payment amount is decided (using the Pay Curve and the \n                    Payment Multipliers), it will be multiplied by this number then \n                    added to and subtracted from the final payment amount to give a min \n                    and max. Then, a random amount between the min and max will be \n                    chosen and awarded to the player.\n                    \n                    Example: After using the Payment Curve and the Payment Multipliers,\n                    we get a payment amount of $10.\n                    The Payment Deviation is 0.2. $10 x 0.2 = 2\n                    Min = $10 - 2 = $8\n                    Max = $10 + 2 = $12\n                    Final Payment will be some random amount between $8 and $12,\n                    inclusively.\n                    ',
                    tunable_type=float,
                    default=0))))
    LIFESTYLE_BRAND_EARNINGS_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification that gets shown when a Sim earns money from their\n        lifestyle brand.\n        \n        Tokens:\n        0 - Sim\n        1 - amount earned\n        2 - brand name\n        '
    )
    BRAND_PAYMENT_MULTIPLIERS = TunableMultiplier.TunableFactory(
        description=
        '\n        A list of test sets which, if they pass, will provide a multiplier to \n        each royalty payment.\n        \n        These tests are only run when a brand is created or changed. If it \n        passes then the payouts will be multiplied going forward until the\n        brand is changed or the brand is stopped and started again.\n        '
    )

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self.clear_brand()
        self._days_active = 0
        self._active_multiplier = 1

    @property
    def active(self):
        return self._product_choice != LifestyleBrandProduct.INVALID and self.target_market != LifestyleBrandTargetMarket.INVALID

    @property
    def days_active(self):
        return self._days_active

    @property
    def product_choice(self):
        return self._product_choice

    @product_choice.setter
    def product_choice(self, value):
        if self._product_choice != value:
            self._restart_days_active()
            self._product_choice = value

    @property
    def target_market(self):
        return self._target_market

    @target_market.setter
    def target_market(self, value):
        if self._target_market != value:
            self._restart_days_active()
            self._target_market = value

    @property
    def logo(self):
        return self._logo

    @logo.setter
    def logo(self, value):
        if self._logo != value:
            self._restart_days_active()
            self._logo = value

    @property
    def brand_name(self):
        return self._brand_name

    @brand_name.setter
    def brand_name(self, value):
        if self._brand_name != value:
            self._restart_days_active()
            self._brand_name = value

    def _restart_days_active(self):
        self._days_active = 0
        self._start_multipliers()

    def update_days_active(self):
        self._days_active += 1

    def _start_multipliers(self):
        self._active_multiplier = LifestyleBrandTracker.BRAND_PAYMENT_MULTIPLIERS.get_multiplier(
            SingleSimResolver(self._sim_info))

    def payout_lifestyle_brand(self):
        if not self.active:
            return
        if FameTunables.LIFESTYLE_BRAND_PERK is None:
            self.clear_brand()
            return
        bucks_tracker = BucksUtils.get_tracker_for_bucks_type(
            FameTunables.LIFESTYLE_BRAND_PERK.associated_bucks_type,
            self._sim_info.id)
        if bucks_tracker is None or not bucks_tracker.is_perk_unlocked(
                FameTunables.LIFESTYLE_BRAND_PERK):
            self.clear_brand()
            return
        payout = self.get_payout_amount()
        if payout > 0:
            self._sim_info.household.funds.add(
                payout, Consts_pb2.FUNDS_LIFESTYLE_BRAND)
            self._display_earnings_notification(payout)
        self.update_days_active()

    def get_payout_amount(self):
        payout = 0
        product_data = LifestyleBrandTracker.LIFESTYLE_BRAND_PAYOUT_MAPPING.get(
            self._target_market)
        if product_data is not None:
            payment_data = product_data.get(self._product_choice)
            if payment_data is not None:
                (final_payment_day, _) = payment_data.curve.points[-1]
                if self._days_active > final_payment_day:
                    self._days_active = int(final_payment_day)
                payout = payment_data.curve.get(self._days_active)
                payout *= self._active_multiplier
                payout = self._apply_deviation_calculation(
                    payout, payment_data.payment_deviation_percent)
        return payout

    def _apply_deviation_calculation(self, payout, deviation_percent):
        deviation = payout * deviation_percent
        min_payment = payout - deviation
        max_payment = payout + deviation
        return int(sims4.random.uniform(min_payment, max_payment))

    def _display_earnings_notification(self, amount_earned):
        resolver = SingleSimResolver(self._sim_info)
        dialog = LifestyleBrandTracker.LIFESTYLE_BRAND_EARNINGS_NOTIFICATION(
            self._sim_info, resolver)
        dialog.show_dialog(additional_tokens=(amount_earned, self.brand_name))

    def clear_brand(self):
        self._product_choice = LifestyleBrandProduct.INVALID
        self._target_market = LifestyleBrandTargetMarket.INVALID
        self._brand_name = None
        self._logo = None
        self._days_active = 0

    def save(self):
        data = SimObjectAttributes_pb2.PersistableLifestyleBrandTracker()
        if self._product_choice is not None:
            data.product = self._product_choice
        if self._target_market is not None:
            data.target_market = self._target_market
        icon_proto = sims4.resources.get_protobuff_for_key(self._logo)
        if icon_proto is not None:
            data.logo = icon_proto
        if self._brand_name is not None:
            data.brand_name = self._brand_name
        data.days_active = self._days_active
        return data

    def load(self, data):
        self._product_choice = data.product
        self._target_market = data.target_market
        self._logo = sims4.resources.Key(data.logo.type, data.logo.instance,
                                         data.logo.group)
        self._brand_name = data.brand_name
        self._days_active = data.days_active

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

    def on_lod_update(self, old_lod, new_lod):
        if new_lod < self._tracker_lod_threshold:
            self.clear_brand()
        elif old_lod < self._tracker_lod_threshold:
            sim_msg = services.get_persistence_service().get_sim_proto_buff(
                self._sim_info.id)
            if sim_msg is not None:
                self.load(sim_msg.attributes.lifestyle_brand_tracker)
Beispiel #18
0
class ObjectDestructionElement(XevtTriggeredElement):
    __qualname__ = 'ObjectDestructionElement'
    FACTORY_TUNABLES = {
        'description':
        'Destroy one or more participants in an interaction.',
        'objects_to_destroy':
        TunableEnumEntry(ParticipantType,
                         ParticipantType.Object,
                         description='The object(s) to destroy.'),
        'award_value':
        OptionalTunable(
            TunableTuple(
                recipients=TunableEnumEntry(
                    ParticipantType,
                    ParticipantType.Actor,
                    description=
                    'Who to award funds to.  If more than one participant is specified, the value will be evenly divided among the recipients.'
                ),
                multiplier=TunableRange(
                    float, 1.0, description='Value multiplier for the award.'),
                tested_multipliers=TunableMultiplier.TunableFactory(
                    description=
                    'Each multiplier that passes its test set will be applied to each royalty payment.'
                )))
    }

    def __init__(self, interaction, **kwargs):
        super().__init__(interaction, **kwargs)
        if self.award_value is not None:
            self.award_value_to = self.award_value.recipients
            self.value_multiplier = self.award_value.multiplier
            self.tested_multipliers = self.award_value.tested_multipliers
        else:
            self.award_value_to = None
        self._destroyed_objects = []

    @classmethod
    def on_affordance_loaded_callback(cls, affordance,
                                      object_destruction_element):
        def get_simoleon_delta(interaction, target=DEFAULT, context=DEFAULT):
            award_value = object_destruction_element.award_value
            if award_value is None:
                return 0
            target = interaction.target if target is DEFAULT else target
            sim = interaction.sim if context is DEFAULT else context.sim
            destructees = interaction.get_participants(
                object_destruction_element.objects_to_destroy,
                sim=sim,
                target=target)
            total_value = sum(obj.current_value for obj in destructees)
            skill_multiplier = 1 if context is DEFAULT else interaction.get_skill_multiplier(
                interaction.monetary_payout_multipliers, sim)
            if total_value > 0:
                total_value *= skill_multiplier
            tested_multiplier = award_value.tested_multipliers.get_multiplier(
                interaction.get_resolver(target=target, context=context))
            return total_value * award_value.multiplier * tested_multiplier

        affordance.register_simoleon_delta_callback(get_simoleon_delta)

    def _destroy_objects(self):
        interaction = self.interaction
        sim = self.interaction.sim
        for object_to_destroy in self._destroyed_objects:
            in_use = object_to_destroy.in_use_by(sim, owner=interaction)
            if object_to_destroy.is_part:
                obj = object_to_destroy.part_owner
            else:
                obj = object_to_destroy
            if in_use:
                obj.transient = True
                obj.remove_from_client()
            else:
                if obj is interaction.target:
                    interaction.set_target(None)
                obj.destroy(source=interaction,
                            cause='Destroying object in basic extra.')

    def _do_behavior(self):
        if self.award_value_to is not None:
            awardees = self.interaction.get_participants(self.award_value_to)
        else:
            awardees = ()
        destructees = self.interaction.get_participants(
            self.objects_to_destroy)
        if destructees:
            for object_to_destroy in destructees:
                if object_to_destroy.is_in_inventory():
                    inventory = object_to_destroy.get_inventory()
                    inventory.try_remove_object_by_id(object_to_destroy.id)
                else:
                    object_to_destroy.remove_from_client()
                self._destroyed_objects.append(object_to_destroy)
                while awardees:
                    multiplier = self.tested_multipliers.get_multiplier(
                        self.interaction.get_resolver())
                    value = int(object_to_destroy.current_value *
                                self.value_multiplier * multiplier)
                    awards = algos.distribute_total_over_parts(
                        value, [1] * len(awardees))
                    object_to_destroy.current_value = 0
                    interaction_tags = set()
                    if self.interaction is not None:
                        interaction_tags |= self.interaction.get_category_tags(
                        )
                    object_tags = frozenset(interaction_tags
                                            | object_to_destroy.get_tags())
                    while True:
                        for (recipient, award) in zip(awardees, awards):
                            recipient.family_funds.add(
                                award,
                                Consts_pb2.TELEMETRY_OBJECT_SELL,
                                recipient,
                                tags=object_tags)
            if self.interaction.is_finishing:
                self._destroy_objects()
            else:
                self.interaction.add_exit_function(self._destroy_objects)
class RestaurantUtils:
    MEAL_COST_MULTIPLIERS = TunableMultiplier.TunableFactory(
        description=
        '\n        Multipliers used to change the value of things in a menu and for the\n        overall cost of the meal.\n        \n        If any member of the party meets the requirement of the multiplier then\n        the multiplier is applied once. The benefit will not be applied for \n        each Sim in the group that meets the multiplier tests.\n        '
    )
Beispiel #20
0
class Adventure(XevtTriggeredElement, HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'description': '\n            A series of individual moments linked together in a game-like\n            fashion.\n            ', '_adventure_moments': TunableMapping(description='\n            The individual adventure moments for this adventure. Every moment\n            used in the adventure must be defined here. For instance, if there\n            is an adventure moment that triggers another adventure moment, the\n            latter must also be defined in this list.\n            ', key_type=AdventureMomentKey, value_type=TunableTuple(aventure_moment=TunableAdventureMomentSnippet(pack_safe=True), sim_specific_cooldown=TunableVariant(description='\n                    The type of sim specific cooldown,\n                    \n                    Hours means cooldown for specified number of sim hours\n                    No cooldown means no cooldown\n                    One shot means a sim will only see it once.\n                    \n                    (Note:  If we hit a visible (or resumed) adventure, after\n                    that point if all actions are on cooldown, the cooldowns will be\n                    ignored.)\n                    ', hours=TunableRange(description='\n                        A cooldown that last for the specified number of hours\n                        ', tunable_type=float, default=50, minimum=1), locked_args={'one_shot': DATE_AND_TIME_ZERO, 'no_cooldown': None}, default='no_cooldown'))), '_initial_moments': TunableList(description='\n            A list of adventure moments that are valid as initiating moments for\n            this adventure.\n            ', tunable=TunableTuple(description='\n                A tuple of moment key and weight. The higher the weight, the\n                more likely it is this moment will be selected as the initial\n                moment.\n                ', adventure_moment_key=TunableEnumEntry(description='\n                    The key of the initial adventure moment.\n                    ', tunable_type=AdventureMomentKey, default=AdventureMomentKey.INVALID), weight=TunableMultiplier.TunableFactory(description='\n                    The weight of this potential initial moment relative\n                    to other items within this list.\n                    '))), '_trigger_interval': TunableInterval(description='\n            The interval, in Sim minutes, between the end of one adventure\n            moment and the beginning of the next one.\n            ', tunable_type=float, default_lower=8, default_upper=12, minimum=0), '_maximum_triggers': Tunable(description='\n            The maximum number of adventure moments that can be triggered by\n            this adventure. Any moment being generated from the adventure beyond\n            this limit will be discarded. Set to 0 to allow for an unlimited\n            number of adventure moments to be triggered.\n            ', tunable_type=int, default=0), '_resumable': Tunable(description='\n            A Sim who enters a resumable adventure will restart the same\n            adventure at the moment they left it at.\n            ', tunable_type=bool, default=True)}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._adventure_moment_count = 0
        self._alarm_handle = None
        self._current_moment_key = None
        self._canceled = False
        self.force_action_result = False

    def _build_outer_elements(self, sequence):
        return build_critical_section_with_finally(sequence, self._end_adventure)

    def _end_adventure(self, *_, **__):
        if self._alarm_handle is not None:
            cancel_alarm(self._alarm_handle)
            self._alarm_handle = None

    def _soft_stop(self):
        self._canceled = True
        return super()._soft_stop()

    @property
    def tracker(self):
        return self.interaction.sim.sim_info.adventure_tracker

    def _get_cheat_display_text(self, display_text, progress, total_progress):
        display_name = self.interaction.create_localized_string(display_text)
        display_name = AdventureMoment.CHEAT_TEXT.text_pattern(display_name, progress, total_progress)
        return lambda *_, **__: display_name

    def get_cheat_responses(self, last_action_id):
        responses = []
        total_moments = len(self._adventure_moment_keys)
        disabled_text = AdventureMoment.CHEAT_TEXT.tooltip
        curr_index = self._adventure_moment_keys.index(self._current_moment_key)
        responses.append(UiDialogResponse(dialog_response_id=last_action_id + AdventureMoment.CHEAT_PREVIOUS_INDEX, text=self._get_cheat_display_text(AdventureMoment.CHEAT_TEXT.previous_display_text, curr_index, total_moments), disabled_text=disabled_text() if curr_index <= 0 else None))
        responses.append(UiDialogResponse(dialog_response_id=last_action_id + AdventureMoment.CHEAT_NEXT_INDEX, text=self._get_cheat_display_text(AdventureMoment.CHEAT_TEXT.next_display_text, curr_index + 2, total_moments), disabled_text=disabled_text() if curr_index >= total_moments - 1 else None))
        return responses

    def run_cheat_previous_moment(self):
        pass

    def run_cheat_next_moment(self):
        pass

    def queue_adventure_moment(self, adventure_moment_key):
        if self._maximum_triggers and self._adventure_moment_count >= self._maximum_triggers:
            return
        time_span = clock.interval_in_sim_minutes(self._trigger_interval.random_float())

        def callback(alarm_handle):
            self._alarm_handle = None
            if not self._canceled:
                self.tracker.remove_adventure_moment(self.interaction)
                self._run_adventure_moment(adventure_moment_key)

        self.tracker.set_adventure_moment(self.interaction, adventure_moment_key)
        self._alarm_handle = add_alarm(self, time_span, callback)

    def _run_adventure_moment(self, adventure_moment_key, count_moment=True):
        if adventure_moment_key in self._adventure_moments:
            adventure_moment_data = self._adventure_moments[adventure_moment_key]
            self._current_moment_key = adventure_moment_key
            self.set_adventure_moment_cooldown(adventure_moment_key)
            adventure_moment_data.aventure_moment(self).run_adventure()
        if count_moment:
            self._adventure_moment_count += 1

    def _get_initial_adventure_moment_key(self):
        initial_adventure_moment_key = _initial_adventure_moment_key_overrides.get(self.interaction.sim)
        if initial_adventure_moment_key is not None and self.is_adventure_moment_available(initial_adventure_moment_key):
            return initial_adventure_moment_key
        if self._resumable:
            initial_adventure_moment_key = self.tracker.get_adventure_moment(self.interaction)
            if initial_adventure_moment_key is not None:
                self.force_action_result = True
                return initial_adventure_moment_key
        participant_resolver = self.interaction.get_resolver()
        return weighted_random_item([(moment.weight.get_multiplier(participant_resolver), moment.adventure_moment_key) for moment in self._initial_moments if self.is_adventure_moment_available(moment.adventure_moment_key)])

    def set_adventure_moment_cooldown(self, adventure_moment_key):
        if adventure_moment_key in self._adventure_moments:
            adventure_moment_data = self._adventure_moments[adventure_moment_key]
            if adventure_moment_data.sim_specific_cooldown is None:
                self.tracker.remove_adventure_moment_cooldown(self.interaction, adventure_moment_key)
                return
            self.tracker.set_adventure_moment_cooldown(self.interaction, adventure_moment_key, adventure_moment_data.sim_specific_cooldown)

    def is_adventure_moment_available(self, adventure_moment_key):
        if adventure_moment_key in self._adventure_moments:
            adventure_moment_data = self._adventure_moments[adventure_moment_key]
            return self.tracker.is_adventure_moment_available(self.interaction, adventure_moment_key, adventure_moment_data.sim_specific_cooldown)
        return True

    def _do_behavior(self):
        if self.tracker is not None:
            initial_moment = self._get_initial_adventure_moment_key()
            if initial_moment is not None:
                self._run_adventure_moment(initial_moment)
Beispiel #21
0
class WeatherAwareComponent(RouteEventProviderMixin,
                            Component,
                            HasTunableFactory,
                            AutoFactoryInit,
                            component_name=WEATHER_AWARE_COMPONENT):
    class TunableWeatherAwareMapping(TunableMapping):
        def __init__(self,
                     start_description=None,
                     end_description=None,
                     **kwargs):
            super().__init__(
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The weather type we are interested in.\n                    ',
                    tunable_type=WeatherType,
                    default=WeatherType.UNDEFINED),
                value_type=TunableTuple(
                    start_loot=TunableList(
                        description=start_description,
                        tunable=LootActions.TunableReference(
                            description=
                            '\n                            The loot action applied.\n                            ',
                            pack_safe=True)),
                    end_loot=TunableList(
                        description=end_description,
                        tunable=LootActions.
                        TunableReference(
                            description=
                            '\n                            The loot action applied.\n                            ',
                            pack_safe=True))),
                **kwargs)
            self.cache_key = 'TunableWeatherAwareMapping'

        def load_etree_node(self, node, source, expect_error):
            value = super().load_etree_node(node, source, expect_error)
            modified_dict = {}
            for (weather_type, loots) in value.items():
                if not loots.start_loot:
                    if loots.end_loot:
                        modified_dict[weather_type] = loots
                modified_dict[weather_type] = loots
            return frozendict(modified_dict)

    FACTORY_TUNABLES = {
        'inside_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get when inside.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts if inside or when the object moves inside \n                during the specified weather.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends if inside or when the object moves outside \n                during the specified weather.\n                '
        ),
        'outside_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get when outside.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts if outside or when the object moves outside \n                during the specified weather.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends if outside or when the object moves inside \n                during the specified weather.\n                '
        ),
        'anywhere_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get regardless of inside/outside location.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            Anywhere actions happen after inside/outside actions when weather starts.\n            Anywhere actions happen before inside/outside actions when weather ends.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts regardless of location.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends regardless of location.\n                '
        ),
        'disable_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            the component is disabled.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'enable_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            the component is enabled.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'lightning_strike_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            they are struck by lightning.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'umbrella_route_events':
        OptionalTunable(
            description=
            '\n            If tuned, we will consider points around inside/outside threshold\n            to handle umbrella route events.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data used to populate fields on the path plan context.\n                ',
                enter_carry_event=RouteEvent.TunablePackSafeReference(
                    description=
                    '\n                    To be moved into weather aware component.\n                    Route event to trigger umbrella carry.\n                    '
                ),
                exit_carry_event=RouteEvent.TunablePackSafeReference(
                    description=
                    '\n                    To be moved into weather aware component.\n                    Route event to trigger umbrella carry.\n                    '
                ))),
        'lightning_strike_multiplier':
        OptionalTunable(
            description=
            '\n            If enabled, we will modify the chance that this object is struck by\n            lightning. Note that the object must be tagged as an object that\n            can be struck. See Lightning module tuning.\n            ',
            tunable=TunableMultiplier.TunableFactory(
                description=
                '\n                A multiplier to the weight for this object to be struck by\n                lightning instead of other objects. \n                \n                Note that this affects Sims as well, but will affect the chance\n                this Sim is struck vs. other Sims, not other objects.\n                '
            )),
        'lightning_effect_override':
        OptionalTunable(
            description=
            '\n            If enabled, when this object is struck by lightning we will play \n            this tuned effect instead of the one specified in module lightning \n            tuning.\n            ',
            tunable=PlayEffect.TunableFactory(
                description=
                '\n                The effect we want to play when this object is struck by \n                lightning.\n                '
            ))
    }

    class LocationUpdateStatus(enum.Int, export=False):
        NOT_IN_PROGRESS = 0
        IN_PROGRESS = 1
        PENDING = 2

    def __init__(self, *args, parent=True, **kwargs):
        super().__init__(*args, **kwargs)
        self._is_outside = None
        self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS
        self._inside_sensitive = True if self.outside_loot or self.inside_loot else False
        self._disabled_count = 0
        self._safety_umbrella_putaway_event = None

    def is_valid_to_add(self):
        if not super().is_valid_to_add():
            return False
        if not self.lightning_strike_loot:
            if self.umbrella_route_events is None:
                return False
            elif not self.umbrella_route_events.enter_carry_event and not self.umbrella_route_events.exit_carry_event:
                return False
        return True

    def enable(self, value):
        if not value:
            if self.enabled:
                if self.owner.manager is not None and services.current_zone(
                ).is_zone_running:
                    self._update_location(disabling=True)
                    if self.owner.is_sim:
                        resolver = SingleSimResolver(self.owner.sim_info)
                    else:
                        resolver = SingleObjectResolver(self.owner)
                    loot_ops_list = LootOperationList(resolver,
                                                      self.disable_loot)
                    loot_ops_list.apply_operations()
                self.on_remove()
            self._disabled_count += 1
        else:
            self._disabled_count -= 1
            if self.enabled:
                if self.owner.is_sim:
                    resolver = SingleSimResolver(self.owner.sim_info)
                else:
                    resolver = SingleObjectResolver(self.owner)
                loot_ops_list = LootOperationList(resolver, self.enable_loot)
                loot_ops_list.apply_operations()
                self.on_add()
                if self._inside_sensitive:
                    self.on_location_changed_callback()
            elif self._disabled_count < 0:
                logger.error(
                    'Unbalanced enabled/disable in weathercomponent.  Called disable once more than enable.'
                )
                self._disabled_count = 0

    @property
    def enabled(self):
        return self._disabled_count == 0

    def on_add(self):
        if self.enabled:
            if self._inside_sensitive:
                self.owner.register_on_location_changed(
                    self.on_location_changed_callback)
            else:
                self.on_location_changed_callback()
            if self._has_routing_events():
                self.owner.routing_component.register_routing_stage_event(
                    RoutingStageEvent.ROUTE_END, self._on_route_finished)

    def on_remove(self):
        if self.enabled:
            self._stop()
            if self.owner.is_on_location_changed_callback_registered(
                    self.on_location_changed_callback):
                self.owner.unregister_on_location_changed(
                    self.on_location_changed_callback)
            if self._has_routing_events():
                self.owner.routing_component.unregister_routing_stage_event(
                    RoutingStageEvent.ROUTE_END, self._on_route_finished)

    def _stop(self):
        weather_service = services.weather_service()
        if weather_service is not None:
            weather_service.flush_weather_aware_message(self.owner)
            if self._is_outside is not None:
                weather_types = set(self.anywhere_loot)
                if self._is_outside:
                    weather_types.update(self.outside_loot)
                else:
                    weather_types.update(self.inside_loot)
                weather_service.deregister_object(self.owner, weather_types)
        self._is_outside = None

    def on_added_to_inventory(self):
        if self.enabled:
            if not self._inside_sensitive:
                self.on_location_changed_callback()
            self._stop()

    def on_removed_from_inventory(self):
        if self.enabled:
            self.on_location_changed_callback()

    def on_finalize_load(self):
        if self.enabled and not self.owner.is_sim:
            self._update_location()

    def on_preroll_autonomy(self):
        if self.enabled:
            is_inside_override = False
            next_interaction = self.owner.queue.peek_head()
            if next_interaction is not None:
                if next_interaction.counts_as_inside:
                    is_inside_override = True
            self._update_location(is_inside_override=is_inside_override)

    def on_buildbuy_exit(self):
        if self.enabled:
            self._update_location()

    def on_location_changed_callback(self, *_, **__):
        if self.enabled and self.owner.manager is not None and services.current_zone(
        ).is_zone_running:
            self._update_location()

    def _update_location(self, is_inside_override=False, disabling=False):
        if disabling:
            is_outside = None
        elif is_inside_override:
            is_outside = False
        elif self.owner.is_in_inventory():
            is_outside = None
        elif not self._inside_sensitive:
            is_outside = True
        else:
            is_outside = self.owner.is_outside
        if is_outside == self._is_outside:
            return
        if self._location_update_status != WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS:
            self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.PENDING
            return
        self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.IN_PROGRESS
        was_outside = self._is_outside
        self._is_outside = is_outside
        recurse = False
        try:
            weather_service = services.weather_service()
            if weather_service is not None:
                weather_types = weather_service.get_current_weather_types()
                weather_service.update_weather_aware_message(self.owner)
            else:
                weather_types = {WeatherType.UNDEFINED}
            if self.owner.is_sim:
                resolver = SingleSimResolver(self.owner.sim_info)
            else:
                resolver = SingleObjectResolver(self.owner)
            if was_outside is not None:
                if was_outside:
                    self._give_loot(weather_types, self.outside_loot, resolver,
                                    False)
                    if weather_service is not None:
                        weather_service.deregister_object(
                            self.owner, self.outside_loot.keys())
                else:
                    self._give_loot(weather_types, self.inside_loot, resolver,
                                    False)
                    if weather_service is not None:
                        weather_service.deregister_object(
                            self.owner, self.inside_loot.keys())
            if is_outside is not None:
                if is_outside:
                    self._give_loot(weather_types, self.outside_loot, resolver,
                                    True)
                    if weather_service is not None:
                        weather_service.register_object(
                            self.owner, self.outside_loot.keys())
                        weather_service.register_object(
                            self.owner, self.anywhere_loot.keys())
                else:
                    self._give_loot(weather_types, self.inside_loot, resolver,
                                    True)
                    if weather_service is not None:
                        weather_service.register_object(
                            self.owner, self.inside_loot.keys())
                        weather_service.register_object(
                            self.owner, self.anywhere_loot.keys())
                if was_outside is None:
                    self._give_loot(weather_types, self.anywhere_loot,
                                    resolver, True)
            else:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                False)
                if weather_service is not None:
                    weather_service.deregister_object(
                        self.owner, self.anywhere_loot.keys())
            recurse = self._location_update_status == WeatherAwareComponent.LocationUpdateStatus.PENDING
        finally:
            self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS
        if recurse:
            self._update_location(is_inside_override=is_inside_override,
                                  disabling=disabling)

    def _give_loot(self, weather_types, loot_dict, resolver, is_start):
        for weather_type in weather_types & loot_dict.keys():
            loot = loot_dict[
                weather_type].start_loot if is_start else loot_dict[
                    weather_type].end_loot
            for loot_action in loot:
                loot_action.apply_to_resolver(resolver)

    @componentmethod_with_fallback(lambda *_, **__: None)
    def give_weather_loot(self, weather_types, is_start):
        if self._is_outside is not None:
            if self.owner.is_sim:
                resolver = SingleSimResolver(self.owner.sim_info)
            else:
                resolver = SingleObjectResolver(self.owner)
            if not is_start:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                is_start)
            if self._is_outside:
                self._give_loot(weather_types, self.outside_loot, resolver,
                                is_start)
            else:
                self._give_loot(weather_types, self.inside_loot, resolver,
                                is_start)
            if is_start:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                is_start)

    @componentmethod_with_fallback(lambda *_, **__: 1.0)
    def get_lightning_strike_multiplier(self):
        if not self.enabled:
            return 0
        elif self.lightning_strike_multiplier is not None:
            return self.lightning_strike_multiplier.get_multiplier(
                SingleObjectResolver(self.owner))
        return 1.0

    def on_struck_by_lightning(self):
        loot_ops_list = LootOperationList(SingleObjectResolver(self.owner),
                                          self.lightning_strike_loot)
        loot_ops_list.apply_operations()

    def _on_route_finished(self, *_, **__):
        self._safety_umbrella_putaway_event = None

    def is_route_event_valid(self, route_event, time, sim, path):
        if not self.enabled:
            return False
        if self._safety_umbrella_putaway_event is route_event:
            location = path.final_location
            if is_location_outside(location.transform.translation,
                                   location.routing_surface.secondary_id):
                self._safety_umbrella_putaway_event = None
                return False
            elif not sims4.math.almost_equal(time, path.duration() - 0.5):
                self._safety_umbrella_putaway_event = None
                return False
        return True

    def _no_regular_put_away_scheduled(self, route_event_context):
        if self._safety_umbrella_putaway_event is None:
            return not route_event_context.route_event_already_scheduled(
                self.umbrella_route_events.exit_carry_event, provider=self)
        for route_event in route_event_context.route_event_of_data_type_gen(
                type(self._safety_umbrella_putaway_event.event_data)):
            if route_event is not self._safety_umbrella_putaway_event:
                return False
        return True

    def _has_routing_events(self):
        if self.umbrella_route_events is None or self.umbrella_route_events.enter_carry_event is None or self.umbrella_route_events.exit_carry_event is None:
            return False
        return True

    def provide_route_events(self,
                             route_event_context,
                             sim,
                             path,
                             start_time=0,
                             end_time=0,
                             **kwargs):
        if not self.enabled:
            return
        if not self._has_routing_events():
            return
        resolver = SingleSimResolver(sim.sim_info)
        can_carry_umbrella = self.umbrella_route_events.enter_carry_event.test(
            resolver)
        added_enter_carry = False
        added_exit_carry = False
        is_prev_point_outside = None
        prev_time = None
        node = None
        prev_node = None
        prev_force_no_carry = False
        for (transform, routing_surface,
             time) in path.get_location_data_along_path_gen(
                 time_step=0.5, start_time=start_time, end_time=end_time):
            force_no_carry = routing_surface.type == SurfaceType.SURFACETYPE_POOL
            if not force_no_carry:
                node = path.node_at_time(time)
                if node is prev_node:
                    force_no_carry = prev_force_no_carry
                elif not force_no_carry:
                    if node.portal_object_id != 0:
                        portal_object = services.object_manager(
                            routing_surface.primary_id).get(
                                node.portal_object_id)
                        if portal_object is not None:
                            portal_instance = portal_object.get_portal_by_id(
                                node.portal_id)
                            force_no_carry = portal_instance is not None and (
                                portal_instance.portal_template is not None and
                                (portal_instance.portal_template.required_flags
                                 is not None and
                                 portal_instance.portal_template.required_flags
                                 & PortalFlags.REQUIRE_NO_CARRY
                                 == PortalFlags.REQUIRE_NO_CARRY))
            level = 0 if routing_surface is None else routing_surface.secondary_id
            is_curr_point_outside = is_location_outside(
                transform.translation, level)
            if is_prev_point_outside is None:
                is_prev_point_outside = is_curr_point_outside
                prev_time = time
            else:
                if can_carry_umbrella:
                    if not added_enter_carry:
                        if is_curr_point_outside:
                            if not route_event_context.route_event_already_scheduled(
                                    self.umbrella_route_events.
                                    enter_carry_event,
                                    provider=self):
                                route_event_context.add_route_event(
                                    RouteEventType.FIRST_OUTDOOR,
                                    self.umbrella_route_events.
                                    enter_carry_event(provider=self,
                                                      time=time))
                                added_enter_carry = True
                if not added_exit_carry:
                    if is_curr_point_outside or not is_prev_point_outside or force_no_carry:
                        if self._no_regular_put_away_scheduled(
                                route_event_context):
                            route_event_context.add_route_event(
                                RouteEventType.LAST_OUTDOOR,
                                self.umbrella_route_events.exit_carry_event(
                                    provider=self, time=prev_time))
                            added_exit_carry = True
                if not can_carry_umbrella or added_enter_carry:
                    if added_exit_carry:
                        break
                is_prev_point_outside = is_curr_point_outside
                prev_time = time
                prev_node = node
                prev_force_no_carry = force_no_carry
        if self._safety_umbrella_putaway_event is None:
            location = path.final_location
            if not is_location_outside(location.transform.translation,
                                       location.routing_surface.secondary_id):
                self._safety_umbrella_putaway_event = self.umbrella_route_events.exit_carry_event(
                    provider=self, time=path.duration() - 0.5)
                route_event_context.add_route_event(
                    RouteEventType.LAST_OUTDOOR,
                    self._safety_umbrella_putaway_event)
Beispiel #22
0
 def schedule_entry_data(pack_safe=False, affected_object_cap=False):
     schedule_entry_tuning = {'tuning_name': 'weighted_situations', 'tuning_type': TunableList(description='\n                A weighted list of situations to be used at the scheduled time.\n                ', tunable=TunableTuple(situation=TunableReference(description='\n                        The situation to start according to the tuned schedule.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), pack_safe=pack_safe), params=TunableTuple(description='\n                        Situation creation parameters.\n                        ', user_facing=Tunable(description="\n                            If enabled, we will start the situation as user facing.\n                            Note: We can only have one user facing situation at a time,\n                            so make sure you aren't tuning multiple user facing\n                            situations to occur at once.\n                            ", tunable_type=bool, default=False)), weight_multipliers=TunableMultiplier.TunableFactory(description="\n                        Tunable tested multiplier to apply to weight.\n                        \n                        *IMPORTANT* The only participants that work are ones\n                        available globally, such as Lot and ActiveHousehold. Only\n                        use these participant types or use tests that don't rely\n                        on any, such as testing all objects via Object Criteria\n                        test or testing active zone with the Zone test.\n                        "), tests=TunableTestSet(description="\n                        A set of tests that must pass for the situation and weight\n                        pair to be available for selection.\n                        \n                        *IMPORTANT* The only participants that work are ones\n                        available globally, such as Lot and ActiveHousehold. Only\n                        use these participant types or use tests that don't rely\n                        on any, such as testing all objects via Object Criteria\n                        test or testing active zone with the Zone test.\n                        ")))}
     if affected_object_cap:
         schedule_entry_tuning['additional_tuning_name'] = 'affected_object_cap'
         schedule_entry_tuning['additional_tuning_type'] = TunableRange(description='\n                Specify the maximum number of objects on the zone lot that \n                can schedule the situations.\n                ', tunable_type=int, minimum=1, default=1)
     return {'schedule_entries': TunableList(description='\n                A list of event schedules. Each event is a mapping of days of the\n                week to a start_time and duration.\n                ', tunable=ScheduleEntry.TunableFactory(schedule_entry_data=schedule_entry_tuning))}
class RoyaltyPayment(HasTunableReference,
                     metaclass=HashedTunedInstanceMetaclass,
                     manager=services.get_instance_manager(
                         sims4.resources.Types.ROYALTY)):
    INSTANCE_TUNABLES = {
        'royalty_recipient':
        TunableEnumEntry(
            description=
            '\n            This is the Sim earning the money.\n            This should always be a Sim (Actor, TargetSim, PickedSim, etc.).\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'royalty_type':
        TunableEnumEntry(
            description=
            '\n            The royalty type this entry belongs to. This is the section in the notification in which it will show.\n            ',
            tunable_type=RoyaltyType,
            default=RoyaltyType.INVALID),
        'royalty_subject':
        TunableEnumEntry(
            description=
            '\n            This is the participant whose name will be used as the object that is earning the money.\n            Supported types are objects (Object, PickedObject, etc.) and Unlockable (for music).\n            Other object types might work but have not been tested.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Object),
        'pay_curve':
        TunableCurve(
            description=
            "\n            This curve represents payment over time.\n            The X-axis is payment number, and the Y-axis is the amount of money to be paid.\n            There MUST be at least two entries in this. One entry for the first payment and\n            one entry for the final payment. If you don't do this, there will be no payments received.\n            The first payment will be X=1. The player will not get any payments where X is tuned to 0.\n            ",
            x_axis_name='Payment Number',
            y_axis_name='Simoleon Amount'),
        'pay_forever':
        Tunable(
            description=
            '\n            If enabled, the final payment will continue to happen forever.\n            If disabled, the final payment will, in fact, be the final payment.\n            ',
            tunable_type=bool,
            default=False),
        'payment_multipliers':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A list of test sets which, if they pass, will provide a multiplier to each royalty payment.\n            These tests are only checked when the royalties start and are applied to every payment.\n            They do not get tested before each payment is sent.\n            All tests will run, so all multipliers that pass will get multiplied together and then multiplied to each payment amount.\n            '
        ),
        'payment_deviation_percent':
        Tunable(
            description=
            '\n            Once the payment amount is decided (using the Pay Curve and the \n            Payment Multipliers), it will be multiplied by this number then \n            added to and subtracted from the final payment amount to give a min \n            and max. Then, a random amount between the min and max will be \n            chosen and awarded to the player.\n            \n            Example: After using the Payment Curve and the Payment Multipliers,\n            we get a payment amount of $10.\n            The Payment Deviation is 0.2. $10 x 0.2 = 2\n            Min = $10 - 2 = $8\n            Max = $10 + 2 = $12\n            Final Payment will be some random amount between $8 and $12,\n            inclusively.\n            ',
            tunable_type=float,
            default=0),
        'payment_tag':
        TunableEnumEntry(
            description=
            '\n            The tag that will be passed along with the royalty payment. This\n            is the tag that will be used for aspirations/achievements.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID)
    }

    @staticmethod
    def get_royalty_payment_tuning(royalty_payment_guid64):
        instance = services.get_instance_manager(
            sims4.resources.Types.ROYALTY).get(royalty_payment_guid64)
        if instance is None:
            logger.error(
                'Tried getting royalty payment tuning for guid {} but got None instead.',
                royalty_payment_guid64)
            return
        return instance
Beispiel #24
0
 def __init__(self, *args, **kwargs):
     super().__init__(work_performance=TunableMultiplier.TunableFactory(description='\n                Multiplier on the base full day work performance (tunable at\n                CareerLevel -> Performance Metrics -> Base Performance).\n                '), money=TunableMultiplier.TunableFactory(description='\n                Multiplier on full day pay, determined by hourly wage (tunable\n                at Career Level -> Simoleons Per Hour), multiplied by work day\n                length (tunable at Career Level -> Work Scheduler), modified by\n                any additional multipliers (e.g. tuning on Career Level ->\n                Simolean Trait Bonus, Career Track -> Overmax, etc.).\n                '), text=TunableLocalizedStringFactory(description='\n                Text shown at end of event notification/dialog if the Sim\n                finishes at this medal.\n                \n                0 param - Sim in the career\n                '), additional_loots=TunableList(description='\n                Any additional loot needed on this medal payout. Currently, this\n                is used to award additional drama nodes/dialogs on this level.\n                ', tunable=LootActions.TunableReference(description='\n                    The loot action applied.\n                    ', pack_safe=True)), **kwargs)
class PaymentElement(XevtTriggeredElement):
    __qualname__ = 'PaymentElement'
    CANNOT_AFFORD_TOOLTIP = TunableLocalizedStringFactory(
        description=
        'Tooltip to display when the player cannot afford to run an interaction.'
    )
    PAY_BILLS = -1
    CATALOG_VALUE = -2
    FACTORY_TUNABLES = {
        'description':
        '\n            Remove any funds this interaction has reserved, either when an\n            appropriate xevt has been triggered, or on interaction complete.\n        \n            To reserve payment and trigger it at the end of all the continuations\n            of an interaction instead use a payment liability.\n            ',
        'cost':
        TunableVariant(
            description='Type of payment this element processes.',
            cost=Tunable(
                int,
                0,
                description=
                'The amount of money it costs to run this interaction.'),
            locked_args={
                'pay_bills': PAY_BILLS,
                'target_catalog_value': CATALOG_VALUE
            },
            default='cost'),
        'display_only':
        Tunable(
            description=
            "\n            A PaymentElement marked as display_only will affect an affordance's\n            display name (by appending the Simoleon cost in parentheses), but\n            will not deduct funds when run.\n            ",
            tunable_type=bool,
            default=False),
        'cost_modifiers':
        TunableMultiplier.TunableFactory(
            description=
            '\n            A tunable list of test sets and associated multipliers to apply to the total cost of this payment.\n            '
        )
    }

    def __init__(self, interaction, **kwargs):
        super().__init__(interaction, **kwargs)
        if not self.display_only:
            self.liability = PaymentLiability(interaction,
                                              self.cost,
                                              init_on_add=True)
            interaction.add_liability(self.liability.LIABILITY_TOKEN,
                                      self.liability)

    @classmethod
    def on_affordance_loaded_callback(cls, affordance, payment_element):
        def get_simoleon_delta(interaction, target=DEFAULT, context=DEFAULT):
            payment_owed = 0
            if payment_element.cost == PaymentElement.PAY_BILLS:
                context = interaction.context if context is DEFAULT else context
                payment_owed = context.sim.household.bills_manager.current_payment_owed
                if payment_owed is not None:
                    payment_owed = -payment_owed
            elif payment_element.cost == PaymentElement.CATALOG_VALUE:
                if target is DEFAULT:
                    target = interaction.target
                payment_owed = -target.definition.price
            else:
                payment_owed = -payment_element.cost
            if payment_owed == 0:
                return payment_owed
            context = interaction.context if context is DEFAULT else context
            return payment_owed * payment_element.cost_modifiers.get_multiplier(
                SingleSimResolver(context.sim.sim_info))

        affordance.register_simoleon_delta_callback(get_simoleon_delta)

    def _do_behavior(self):
        if self.display_only:
            return
        if self.liability.reserved_funds is not None:
            if self.cost == PaymentElement.PAY_BILLS:
                self.liability.process_bills_payment()
            else:
                self.liability.reserved_funds.apply()
                self.liability.reserved_funds = None
Beispiel #26
0
class RandomWeightedLoot(HasTunableReference,
                         HasTunableSingletonFactory,
                         AutoFactoryInit,
                         metaclass=HashedTunedInstanceMetaclass,
                         manager=services.get_instance_manager(
                             sims4.resources.Types.ACTION)):
    INSTANCE_TUNABLES = {
        'random_loot_actions':
        TunableList(
            description=
            '\n            List of weighted loot actions that can be run.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Weighted actions that will be randomly selected when\n                the loot is executed.  The loots will be tested\n                before running to guarantee the random action is valid. \n                ',
                action=LootActionVariant(
                    do_nothing=DoNothingLootOp.TunableFactory()),
                weight=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    The weight of this potential initial moment relative\n                    to other items within this list.\n                    '
                )))
    }
    _simoleon_loot = None

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._simoleon_loot = None
        for random_action in cls.random_loot_actions:
            if hasattr(random_action.action, 'get_simoleon_delta'):
                if cls._simoleon_loot is None:
                    cls._simoleon_loot = []
                cls._simoleon_loot.append(random_action.action)

    @classproperty
    def loot_type(self):
        return LootType.ACTIONS

    @classmethod
    @assertions.not_recursive
    def _validate_recursion(cls):
        for random_action in cls.random_loot_actions:
            if random_action.action.loot_type == LootType.ACTIONS:
                try:
                    random_action.action._validate_recursion()
                except AssertionError:
                    logger.error(
                        '{} is an action in {} but that creates a circular dependency',
                        random_action.action,
                        cls,
                        owner='camilogarcia')

    @flexmethod
    def get_loot_ops_gen(cls, inst, resolver=None, auto_select=True):
        inst_or_cls = inst if inst is not None else cls
        if resolver is None:
            for random_action in inst_or_cls.random_loot_actions:
                if random_action.action.loot_type == LootType.ACTIONS:
                    yield from random_action.action.get_loot_ops_gen(
                        resolver=resolver)
                else:
                    yield (random_action.action, False)
        elif auto_select:
            weighted_random_actions = [
                (ra.weight.get_multiplier(resolver), ra.action)
                for ra in inst_or_cls.random_loot_actions
            ]
            actions = []
            while weighted_random_actions:
                potential_action_index = sims4.random.weighted_random_index(
                    weighted_random_actions)
                if potential_action_index is None:
                    return
                potential_action = weighted_random_actions.pop(
                    potential_action_index)[1]
                if potential_action is None:
                    continue
                if potential_action.loot_type == LootType.ACTIONS:
                    valid_actions = []
                    for (action, _) in potential_action.get_loot_ops_gen(
                            resolver=resolver):
                        if action.test_resolver(resolver):
                            valid_actions.append(action)
                    actions = valid_actions
                    break
                elif potential_action.test_resolver(resolver):
                    actions = (potential_action, )
                    break
            for action in actions:
                if action.loot_type == LootType.ACTIONS:
                    yield from action.get_loot_ops_gen(resolver=resolver)
                else:
                    yield (action, True)
        else:
            yield (inst_or_cls, False)

    @flexmethod
    def apply_to_resolver(cls, inst, resolver, skip_test=False):
        inst_or_cls = inst if inst is not None else cls
        for (action, test_ran) in inst_or_cls.get_loot_ops_gen(resolver):
            try:
                action.apply_to_resolver(resolver, skip_test=test_ran)
            except BaseException as ex:
                logger.exception(
                    'Exception when applying action {} for loot {}', action,
                    cls)
                raise ex

    @classmethod
    def test_resolver(cls, *_, **__):
        return True

    @flexmethod
    def apply_to_interaction_statistic_change_element(cls, inst, resolver):
        inst_or_cls = inst if inst is not None else cls
        inst_or_cls.apply_to_resolver(resolver, skip_test=True)

    @classmethod
    def get_stat(cls, _interaction):
        pass

    @classmethod
    def get_simoleon_delta(cls, *args, **kwargs):
        total_funds_category = None
        total_funds_delta = 0
        if cls._simoleon_loot is not None:
            for action in cls._simoleon_loot:
                (funds_delta,
                 funds_category) = action.get_simoleon_delta(*args, **kwargs)
                if funds_category is not None:
                    total_funds_category = funds_category
                total_funds_delta += funds_delta
        return (total_funds_delta, total_funds_category)
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()
Beispiel #28
0
class MovingObjectsSituation(SituationComplexCommon):
    INSTANCE_TUNABLES = {'_preparation_state': _PreparationState.TunableFactory(tuning_group=GroupNames.STATE), '_waiting_to_move_state': _WaitingToMoveState.TunableFactory(tuning_group=GroupNames.STATE), '_tests_to_continue': TunableTestSet(description='\n            A list of tests that must pass in order to continue the situation\n            after the tuned duration for the waiting state has elapsed.\n            ', tuning_group=GroupNames.STATE), 'starting_requirements': TunableTestSet(description='\n            A list of tests that must pass in order for the situation\n            to start.\n            ', tuning_group=GroupNames.SITUATION), 'object_tags': TunableTags(description='\n            Tags used to find objects which will move about.\n            ', tuning_group=GroupNames.SITUATION), 'placement_strategy_locations': TunableList(description='\n            A list of weighted location strategies.\n            ', tunable=TunableTuple(weight=TunableMultiplier.TunableFactory(description='\n                    The weight of this strategy relative to other locations.\n                    '), placement_strategy=_PlacementStrategyLocation.TunableFactory(description='\n                    The placement strategy for the object.\n                    ')), minlength=1, tuning_group=GroupNames.SITUATION), 'fade': OptionalTunable(description='\n            If enabled, the objects will fade-in/fade-out as opposed to\n            immediately moving to their location.\n            ', tunable=TunableTuple(out_time=TunableSimMinute(description='\n                    Time over which the time will fade out.\n                    ', default=1), in_time=TunableSimMinute(description='\n                    Time over which the time will fade in.\n                    ', default=1)), enabled_by_default=True, tuning_group=GroupNames.SITUATION), 'vfx_on_move': OptionalTunable(description='\n            If tuned, apply this one-shot vfx on the moving object when it\n            is about to move.\n            ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.SITUATION), 'situation_end_loots_to_apply_on_objects': TunableSet(description='\n            The loots to apply on the tagged objects when the situation ends \n            or is destroyed.\n            \n            E.g. use this to reset objects to a specific state after \n            the situation is over.\n            \n            The loot will be processed with the active sim as the actor,\n            and the object as the target.\n            ', tunable=TunableReference(manager=services.get_instance_manager(Types.ACTION), pack_safe=True), tuning_group=GroupNames.SITUATION)}
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        reader = self._seed.custom_init_params_reader
        if reader is None:
            self._target_id = self._seed.extra_kwargs.get('default_target_id', None)
        else:
            self._target_id = reader.read_uint64(OBJECT_TOKEN, None)

    @classmethod
    def _states(cls):
        return (SituationStateData(0, _PreparationState, factory=cls._preparation_state), SituationStateData(1, _WaitingToMoveState, factory=cls._waiting_to_move_state))

    @classmethod
    def default_job(cls):
        pass

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return []

    @classmethod
    def situation_meets_starting_requirements(cls, **kwargs):
        if not cls.starting_requirements:
            return True
        else:
            resolver = SingleSimResolver(services.active_sim_info())
            if not cls.starting_requirements.run_tests(resolver):
                return False
        return True

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        if self._target_id is not None:
            writer.write_uint64(OBJECT_TOKEN, self._target_id)

    def start_situation(self):
        super().start_situation()
        self._change_state(self._preparation_state())

    def load_situation(self):
        if not self.situation_meets_starting_requirements():
            return False
        return super().load_situation()

    def on_objects_ready(self):
        self._change_state(self._waiting_to_move_state())

    def on_ready_to_move(self):
        if self._tests_to_continue.run_tests(GlobalResolver()):
            self._move_objects()
            self._change_state(self._waiting_to_move_state())
        else:
            self._self_destruct()

    def _get_placement_resolver(self):
        additional_participants = {}
        if self._target_id is not None:
            target = services.object_manager().get(self._target_id)
            additional_participants[ParticipantType.Object] = (target,)
            if target is not None:
                if target.is_sim:
                    additional_participants[ParticipantType.TargetSim] = (target.sim_info,)
        return SingleSimResolver(services.active_sim_info(), additional_participants=additional_participants)

    def _destroy(self):
        objects_of_interest = services.object_manager().get_objects_matching_tags(self.object_tags, match_any=True)
        if not objects_of_interest:
            return
        active_sim_info = services.active_sim_info()
        for obj in objects_of_interest:
            resolver = SingleActorAndObjectResolver(active_sim_info, obj, self)
            for loot in self.situation_end_loots_to_apply_on_objects:
                loot.apply_to_resolver(resolver)
        super()._destroy()

    def _move_objects(self):
        objects_to_move = services.object_manager().get_objects_matching_tags(self.object_tags, match_any=True)
        if not objects_to_move:
            return
        resolver = self._get_placement_resolver()
        choices = [(location.weight.get_multiplier(resolver), location.placement_strategy) for location in self.placement_strategy_locations]
        chosen_strategy = random.weighted_random_item(choices)
        do_fade = self.fade is not None
        out_sequence = []
        moves = []
        in_sequence = []
        for object_to_move in objects_to_move:
            object_to_move.cancel_interactions_running_on_object(FinishingType.OBJECT_CHANGED, cancel_reason_msg='Object changing location.')
            if self.vfx_on_move is not None:
                out_sequence.append(lambda _, object_to_move=object_to_move: self.vfx_on_move(object_to_move).start_one_shot())
            if do_fade:
                out_sequence.append(lambda _, object_to_move=object_to_move: object_to_move.fade_out(self.fade.out_time))
            moves.append(lambda _, object_to_move=object_to_move: chosen_strategy.try_place_object(object_to_move, resolver))
            if do_fade:
                in_sequence.append(lambda _, object_to_move=object_to_move: object_to_move.fade_in(self.fade.in_time))
        sequence = []
        if out_sequence:
            sequence.append(out_sequence)
            sequence.append(SoftSleepElement(clock.interval_in_sim_minutes(self.fade.out_time)))
        sequence.append(moves)
        if in_sequence:
            sequence.append(in_sequence)
        element = build_element(sequence, critical=CleanupType.RunAll)
        services.time_service().sim_timeline.schedule(element)
Beispiel #29
0
class AppearanceModifier(HasTunableSingletonFactory, AutoFactoryInit):
    class BaseAppearanceModification(HasTunableSingletonFactory,
                                     AutoFactoryInit):
        FACTORY_TUNABLES = {
            '_is_combinable_with_same_type':
            Tunable(
                description=
                '\n                True if this modifier type is able to be combined with another\n                of its type. If True, and two modifiers conflict, then the tuned\n                priority will be used to resolve the conflict. If False, only\n                a single modifier of this type with the highest priority will be shown.\n                ',
                tunable_type=bool,
                default=True),
            'outfit_type_compatibility':
            OptionalTunable(
                description=
                '\n                If enabled, will verify when switching outfits if the new\n                outfit is compatible with this appearance modifier.\n                ',
                disabled_name="Don't_Test",
                tunable=TunableWhiteBlackList(
                    description=
                    '\n                    The outfit category must match the whitelist and blacklist\n                    to be applied.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The outfit category want to test against the \n                        apperance modifier.\n                        ',
                        tunable_type=OutfitCategory,
                        default=OutfitCategory.EVERYDAY))),
            'appearance_modifier_tag':
            OptionalTunable(
                description=
                '\n                If enabled, a tag used to reference this appearance modifier.\n                ',
                tunable=TunableTag(
                    description=
                    '\n                    Tag associated with this appearance modifier.\n                    '
                ))
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        @property
        def is_permanent_modification(self):
            return False

        @property
        def modifier_type(self):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        @property
        def is_combinable_with_same_type(self):
            return self._is_combinable_with_same_type

        @property
        def combinable_sorting_key(self):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        def is_compatible_with_outfit(self, outfit_category):
            if self.outfit_type_compatibility is None:
                return True
            return self.outfit_type_compatibility.test_item(outfit_category)

    class SetCASPart(BaseAppearanceModification):
        FACTORY_TUNABLES = {
            'cas_part':
            TunableCasPart(
                description=
                '\n                The CAS part that will be modified.\n                '
            ),
            'should_toggle':
            Tunable(
                description=
                "\n                Whether or not to toggle this part. e.g. if it exists, remove\n                it, if it doesn't exist, add it. If set to false, the part will\n                be added if it doesn't exist, but not removed if it does exist.\n                ",
                tunable_type=bool,
                default=False),
            'replace_with_random':
            Tunable(
                description=
                '\n                Whether or not to replace the tuned cas part with a random\n                variant.\n                ',
                tunable_type=bool,
                default=False),
            'remove_conflicting':
            Tunable(
                description=
                '\n                If checked, conflicting parts are removed from the outfit. For\n                instance, a full body outfit might be removed if a part would\n                conflict with it.\n                \n                e.g.\n                 The Cone of Shame removes conflicting full-body pet outfits.\n                ',
                tunable_type=bool,
                default=False),
            'update_genetics':
            Tunable(
                description=
                '\n                Whether or not to update the genetics of the sim with this\n                modification to make it a permanent modification. NOTE: DO NOT\n                tune permanent with temporary modifications on the same\n                appearance modifier.\n                ',
                tunable_type=bool,
                default=False),
            'expect_invalid_parts':
            Tunable(
                description=
                "\n                Whether or not parts that are invalid for a sim should log an\n                error.  If we are expecting invalid parts, (say, buff gives one\n                part that applies to adults and a different part for children,)\n                then we should set this to True so that it doesn't throw the\n                error when it tries to apply the adult part on the child and\n                vice versa.\n                ",
                tunable_type=bool,
                default=False)
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            if set_caspart(source_sim_info._base,
                           modified_sim_info._base,
                           self.cas_part,
                           self.should_toggle,
                           self.replace_with_random,
                           self.update_genetics,
                           random_seed,
                           remove_conflicting=self.remove_conflicting):
                return BodyTypeFlag.make_body_type_flag(
                    get_caspart_bodytype(self.cas_part))
            if not self.expect_invalid_parts:
                sis = []
                instanced_sim = source_sim_info.get_sim_instance()
                if instanced_sim is not None:
                    sis = instanced_sim.get_all_running_and_queued_interactions(
                    )
                active_mods = source_sim_info.appearance_tracker.active_displayed_appearance_modifiers(
                )
                logger.error(
                    'Unable to set cas part {}\nSim: {}, Gender: {}, Age: {} \nActive Modifiers: \n{} \nInteractions: \n{}',
                    self, source_sim_info, source_sim_info.gender,
                    source_sim_info.age, active_mods, sis)
            return BodyTypeFlag.NONE

        @property
        def is_permanent_modification(self):
            return self.update_genetics

        @property
        def modifier_type(self):
            return AppearanceModifierType.SET_CAS_PART

        @property
        def combinable_sorting_key(self):
            return get_caspart_bodytype(self.cas_part)

        def __repr__(self):
            return standard_repr(self,
                                 cas_part=self.cas_part,
                                 should_toggle=self.should_toggle,
                                 replace_with_random=self.replace_with_random,
                                 update_genetics=self.update_genetics)

    class ReplaceCASPart(BaseAppearanceModification):
        @staticmethod
        def _verify_tunable_callback(instance_class, tunable_name, source,
                                     value, **kwargs):
            if len(value.replace_part_map
                   ) == 0 and value.default_set_part is None:
                logger.error(
                    'Cannot use ReplaceCASPart without a mapping or a default for {}',
                    instance_class,
                    owner='bosee')

        FACTORY_TUNABLES = {
            'replace_part_map':
            TunableMapping(
                description=
                "\n                The CAS part (value) that will replace another CAS part (key)\n                if sim has that equipped. It currently only replaces the first \n                one which it finds. Nothing will be replaced if the sim doesn't\n                have any of the key CAS parts set. \n                ",
                key_type=TunableCasPart(
                    description=
                    '\n                    CAS part to look up.\n                    '
                ),
                value_type=TunableCasPart(
                    description=
                    '\n                    If key CAS part is set, replace it with this CAS part.\n                    '
                )),
            'default_set_part':
            OptionalTunable(
                description=
                '\n                If set, this CAS part will be set if no parts are replaced with the \n                previous mapping.\n                ',
                tunable=TunableCasPart(
                    description=
                    "\n                    The CAS part that will be modified. This doesn't take into account\n                    what has already been set on the sim.\n                    "
                )),
            'update_genetics':
            Tunable(
                description=
                '\n                Whether or not to update the genetics of the sim with this\n                modification to make it a permanent modification. NOTE: DO NOT\n                tune permanent with temporary modifications on the same\n                appearance modifier.\n                ',
                tunable_type=bool,
                default=False),
            'expect_invalid_parts':
            Tunable(
                description=
                "\n                Whether or not parts that are invalid for a sim should log an\n                error.  If we are expecting invalid parts, (say, buff gives one\n                part that applies to adults and a different part for children,)\n                then we should set this to True so that it doesn't throw the\n                error when it tries to apply the adult part on the child and\n                vice versa.\n                ",
                tunable_type=bool,
                default=False),
            'verify_tunable_callback':
            _verify_tunable_callback
        }

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

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            self._last_modified_type = None
            part_to_set = None
            for (key_part, value_part) in self.replace_part_map.items():
                if source_sim_info.get_outfits().has_cas_part(key_part):
                    part_to_set = value_part
                    break
            if part_to_set is None:
                if self.default_set_part is not None:
                    part_to_set = self.default_set_part
            if part_to_set is None:
                return BodyTypeFlag.NONE
            self._last_modified_type = get_caspart_bodytype(part_to_set)
            if set_caspart(source_sim_info._base,
                           modified_sim_info._base,
                           part_to_set,
                           False,
                           False,
                           self.update_genetics,
                           random_seed,
                           remove_conflicting=True):
                return BodyTypeFlag.make_body_type_flag(
                    get_caspart_bodytype(part_to_set))
            if not self.expect_invalid_parts:
                sis = []
                instanced_sim = source_sim_info.get_sim_instance()
                if instanced_sim is not None:
                    sis = instanced_sim.get_all_running_and_queued_interactions(
                    )
                active_mods = source_sim_info.appearance_tracker.active_displayed_appearance_modifiers(
                )
                logger.error(
                    'Unable to set cas part {}\nSim: {}, Gender: {}, Age: {} \nActive Modifiers: \n{} \nInteractions: \n{}',
                    self, source_sim_info, source_sim_info.gender,
                    source_sim_info.age, active_mods, sis)
            return BodyTypeFlag.NONE

        @property
        def is_permanent_modification(self):
            return self.update_genetics

        @property
        def modifier_type(self):
            return AppearanceModifierType.SET_CAS_PART

        @property
        def combinable_sorting_key(self):
            return self._last_modified_type

        def __repr__(self):
            return standard_repr(
                self,
                default_set_part=self.default_set_part,
                update_genetics=self.update_genetics,
                expect_invalid_parts=self.expect_invalid_parts)

    class RandomizeCASPart(BaseAppearanceModification):
        FACTORY_TUNABLES = {
            'body_type':
            TunableEnumEntry(
                description=
                '\n                The body type that will have its part randomized.\n                ',
                tunable_type=BodyType,
                default=BodyType.NONE,
                invalid_enums=(BodyType.NONE, )),
            'tag_categories_to_keep':
            TunableSet(
                description=
                '\n                Match tags from the existing CAS part of the specified body \n                type that belong to these tag categories when searching\n                for a new random part.\n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    Tags that belong to this category that are on the existing\n                    CAS part of the specified body type will be used to find\n                    a new random part.\n                    ',
                    tunable_type=TagCategory,
                    default=TagCategory.INVALID,
                    invalid_enums=(TagCategory.INVALID, ))),
            'tags':
            TunableTags(
                description=
                '\n                List of tags to use when randomizing a CAS part for the tuned\n                body type.\n                '
            )
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            if randomize_caspart(source_sim_info._base,
                                 modified_sim_info._base, self.body_type,
                                 list(self.tag_categories_to_keep),
                                 random_seed, list(self.tags)):
                return BodyTypeFlag.make_body_type_flag(self.body_type)
            return BodyTypeFlag.NONE

        @property
        def modifier_type(self):
            return AppearanceModifierType.RANDOMIZE_CAS_PART

        @property
        def combinable_sorting_key(self):
            return self.body_type

        def __repr__(self):
            return standard_repr(self, body_type=self.body_type)

    class RandomizeBodyTypeColor(BaseAppearanceModification):
        FACTORY_TUNABLES = {
            'body_type':
            TunableEnumEntry(
                description=
                '\n                The body type that will have its color randomized.\n                ',
                tunable_type=BodyType,
                default=BodyType.NONE)
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            if randomize_part_color(source_sim_info._base,
                                    modified_sim_info._base, self.body_type,
                                    random_seed):
                return BodyTypeFlag.make_body_type_flag(self.body_type)
            return BodyTypeFlag.NONE

        @property
        def modifier_type(self):
            return AppearanceModifierType.RANDOMIZE_BODY_TYPE_COLOR

        @property
        def combinable_sorting_key(self):
            return self.body_type

        def __repr__(self):
            return standard_repr(self, body_type=self.body_type)

    class RandomizeSkintoneFromTags(BaseAppearanceModification):
        FACTORY_TUNABLES = {
            'tag_list':
            TunableList(
                TunableEnumEntry(
                    description=
                    '\n                    A specific tag.\n                    ',
                    tunable_type=tag.Tag,
                    default=tag.Tag.INVALID)),
            'locked_args': {
                '_is_combinable_with_same_type': False
            }
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            randomize_skintone_from_tags(source_sim_info._base,
                                         modified_sim_info._base,
                                         list(self.tag_list), random_seed)
            return BodyTypeFlag.NONE

        @property
        def modifier_type(self):
            return AppearanceModifierType.RANDOMIZE_SKINTONE_FROM_TAGS

        def __repr__(self):
            return standard_repr(self, tag_list=self.tag_list)

    class GenerateOutfit(BaseAppearanceModification):
        FACTORY_TUNABLES = {
            'outfit_generator':
            OutfitGenerator.TunableFactory(
                description=
                '\n                Inputs to generate the type of outfit we want.\n                '
            ),
            'outfit_override':
            OptionalTunable(
                description=
                "\n                If enabled, we will generate the outfit on the tuned outfit\n                category and index. Otherwise, we use the Sim's current outfit\n                in the generator.\n                ",
                disabled_name='Current_Outfit',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    The outfit category we want to generate the outfit on.\n                    ',
                    tunable_type=OutfitCategory,
                    default=OutfitCategory.EVERYDAY))
        }

        @property
        def combinable_sorting_key(self):
            if self.outfit_override is not None:
                return self.outfit_override
            return OutfitCategory.EVERYDAY

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            (outfit_category, outfit_index) = (
                self.outfit_override, 0
            ) if self.outfit_override is not None else source_sim_info.get_current_outfit(
            )
            SimInfoBaseWrapper.copy_base_attributes(modified_sim_info,
                                                    source_sim_info)
            SimInfoBaseWrapper.copy_physical_attributes(
                modified_sim_info, source_sim_info)
            modified_sim_info.load_outfits(source_sim_info.save_outfits())
            body_type_flags = self.outfit_generator.get_body_type_flags()
            with modified_sim_info.set_temporary_outfit_flags(
                    outfit_category, outfit_index, body_type_flags):
                self.outfit_generator(modified_sim_info,
                                      outfit_category,
                                      outfit_index=outfit_index,
                                      seed=random_seed)
            return body_type_flags

        @property
        def modifier_type(self):
            return AppearanceModifierType.GENERATE_OUTFIT

    @staticmethod
    def _verify_tunable_callback(instance_class, tunable_name, source, value,
                                 **kwargs):
        is_permanent_modification = None
        for tuned_modifiers in value.appearance_modifiers:
            if len(tuned_modifiers
                   ) == 1 and tuned_modifiers[0].weight.base_value != 1:
                logger.error(
                    'An appearance modifier has only one entry\n                                    in the list of modifiers and the weight of\n                                    that modifier is != 0. Instead it is {}',
                    tuned_modifiers[0].weight.base_value,
                    owner='rfleig')
            for entry in tuned_modifiers:
                if is_permanent_modification is None:
                    is_permanent_modification = entry.modifier.is_permanent_modification
                elif is_permanent_modification != entry.modifier.is_permanent_modification:
                    logger.error(
                        'An appearance modifier is attempting to combine a permanent\n                                        modifier with a temporary one. This is not supported.',
                        owner='jwilkinson')
                    return

    FACTORY_TUNABLES = {
        'priority':
        TunableEnumEntry(
            description=
            '\n            The priority of the appearance request. Higher priority will\n            take precedence over lower priority. Equal priority will favor\n            recent requests.\n            ',
            tunable_type=AppearanceModifierPriority,
            default=AppearanceModifierPriority.INVALID),
        'appearance_modifiers':
        TunableList(
            description=
            '\n            The specific appearance modifiers to use for this buff.\n            ',
            tunable=TunableList(
                description=
                '\n                A tunable list of weighted modifiers. When applying modifiers\n                one of the modifiers in this list will be applied. The weight\n                will be used to run a weighted random selection.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A Modifier to apply and weight for the weighted random \n                    selection.\n                    ',
                    modifier=TunableVariant(
                        set_cas_part=SetCASPart.TunableFactory(),
                        replace_cas_part=ReplaceCASPart.TunableFactory(),
                        randomize_cas_part=RandomizeCASPart.TunableFactory(),
                        randomize_body_type_color=RandomizeBodyTypeColor.
                        TunableFactory(),
                        randomize_skintone_between_tags=
                        RandomizeSkintoneFromTags.TunableFactory(),
                        generate_outfit=GenerateOutfit.TunableFactory(),
                        default='set_cas_part'),
                    weight=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        A weight with testable multipliers that is used to \n                        determine how likely this entry is to be picked when \n                        selecting randomly.\n                        '
                    )))),
        'apply_to_all_outfits':
        Tunable(
            description=
            '\n            If checked, the appearance modifiers will be applied to all outfits,\n            otherwise they will only be applied to the current outfit.\n            ',
            tunable_type=bool,
            default=True),
        'verify_tunable_callback':
        _verify_tunable_callback
    }
Beispiel #30
0
class Puddle(objects.game_object.GameObject):
    WEED_DEFINITIONS = TunableDefinitionList(description='\n        Possible weed objects which can be spawned by evaporation.')
    PLANT_DEFINITIONS = TunableDefinitionList(description='\n        Possible plant objects which can be spawned by evaporation.')
    INSTANCE_TUNABLES = {'indoor_evaporation_time': TunableInterval(description='\n            Number of SimMinutes this puddle should take to evaporate when \n            created indoors.\n            ', tunable_type=TunableSimMinute, default_lower=200, default_upper=300, minimum=1, tuning_group=GroupNames.DEPRECATED), 'outdoor_evaporation_time': TunableInterval(description='\n            Number of SimMinutes this puddle should take to evaporate when \n            created outdoors.\n            ', tunable_type=TunableSimMinute, default_lower=30, default_upper=60, minimum=1, tuning_group=GroupNames.DEPRECATED), 'evaporation_outcome': TunableTuple(nothing=TunableRange(int, 5, minimum=1, description='Relative chance of nothing.'), weeds=TunableRange(int, 2, minimum=0, description='Relative chance of weeds.'), plant=TunableRange(int, 1, minimum=0, description='Relative chance of plant.'), tuning_group=GroupNames.PUDDLES), 'intial_stat_value': TunableTuple(description='\n            This is the starting value for the stat specified.  This controls \n            how long it takes to mop this puddle.\n            ', stat=Statistic.TunableReference(description='\n                The stat used for mopping puddles.\n                '), value=Tunable(description='\n                The initial value this puddle should have for the mopping stat.\n                The lower the value (-100,100), the longer it takes to mop up.\n                ', tunable_type=int, default=-20), tuning_group=GroupNames.PUDDLES), 'evaporation_data': TunableTuple(description='\n            This is the information for evaporation.  This controls how long this\n            puddle takes to evaporate.\n            ', commodity=Commodity.TunableReference(description='\n                The commodity used for evaporation.\n                '), initial_value=TunableInterval(description='\n                Initial value of this commodity.  Time it takes to evaporate\n                will be based on how fast this commodity decays.\n                (Based on loot given in weather aware component)\n                ', tunable_type=float, default_lower=30, default_upper=60, minimum=1), tuning_group=GroupNames.PUDDLES), 'puddle_liquid': TunableEnumEntry(description='\n        The liquid that the puddle is made of.\n        ', tunable_type=PuddleLiquid, default=PuddleLiquid.INVALID, invalid_enums=(PuddleLiquid.INVALID,), tuning_group=GroupNames.PUDDLES), 'puddle_size': TunableEnumEntry(description='\n        The size of the puddle.\n        ', tunable_type=PuddleSize, default=PuddleSize.NoPuddle, invalid_enums=(PuddleSize.NoPuddle,), tuning_group=GroupNames.PUDDLES), 'puddle_grow_chance': TunableMultiplier.TunableFactory(description='\n        The chance of puddle to grow.\n        ', tuning_group=GroupNames.PUDDLES)}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._evaporate_callback_handle = None
        self.statistic_tracker.set_value(self.intial_stat_value.stat, self.intial_stat_value.value)

    @property
    def size_count(self):
        if self.puddle_size == PuddleSize.SmallPuddle:
            return 1
        if self.puddle_size == PuddleSize.MediumPuddle:
            return 2
        elif self.puddle_size == PuddleSize.LargePuddle:
            return 3

    def place_puddle(self, target, max_distance, ids_to_ignore=DEFAULT):
        destroy_puddle = True
        try:
            if ids_to_ignore is DEFAULT:
                ids_to_ignore = (self.id,)
            else:
                ids_to_ignore.append(self.id)
            flags = placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_POSITIONS
            flags = flags | placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_INTENDED_POSITIONS
            flags = flags | placement.FGLSearchFlag.STAY_IN_SAME_CONNECTIVITY_GROUP
            if target.is_on_active_lot():
                flags = flags | placement.FGLSearchFlag.SHOULD_TEST_BUILDBUY
            else:
                flags = flags | placement.FGLSearchFlag.SHOULD_TEST_ROUTING
                flags = flags | placement.FGLSearchFlag.USE_SIM_FOOTPRINT
            flags = flags | placement.FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS
            flags = flags | placement.FGLSearchFlag.DONE_ON_MAX_RESULTS
            radius_target = target
            while radius_target.parent is not None:
                radius_target = radius_target.parent
            if radius_target.is_part:
                radius_target = radius_target.part_owner
            routing_surface = target.routing_surface
            routing_surface = SurfaceIdentifier(routing_surface.primary_id, routing_surface.secondary_id, SurfaceType.SURFACETYPE_WORLD)
            starting_location = placement.create_starting_location(position=target.position + target.forward*radius_target.object_radius, orientation=sims4.random.random_orientation(), routing_surface=routing_surface)
            fgl_context = placement.create_fgl_context_for_object(starting_location, self, search_flags=flags, ignored_object_ids=ids_to_ignore, max_distance=max_distance)
            (position, orientation) = placement.find_good_location(fgl_context)
            if position is not None:
                destroy_puddle = False
                self.place_puddle_at(position, orientation, routing_surface)
                return True
            return False
        finally:
            if destroy_puddle:
                self.destroy(source=self, cause='Failed to place puddle.')

    def place_puddle_at(self, position, orientation, routing_surface):
        self.location = sims4.math.Location(sims4.math.Transform(position, orientation), routing_surface)
        self.fade_in()
        self.start_evaporation()

    def try_grow_puddle(self):
        if self.puddle_size == PuddleSize.LargePuddle:
            return
        resolver = SingleObjectResolver(self)
        chance = self.puddle_grow_chance.get_multiplier(resolver)
        if random.random() > chance:
            return
        else:
            if self.puddle_size == PuddleSize.MediumPuddle:
                puddle = create_puddle(PuddleSize.LargePuddle, puddle_liquid=self.puddle_liquid)
            else:
                puddle = create_puddle(PuddleSize.MediumPuddle, puddle_liquid=self.puddle_liquid)
            if puddle.place_puddle(self, 1, ids_to_ignore=[self.id]):
                if self._evaporate_callback_handle is not None:
                    self.commodity_tracker.remove_listener(self._evaporate_callback_handle)
                self.destroy(self, cause='Puddle is growing.', fade_duration=ClientObjectMixin.FADE_DURATION)
                return puddle

    def start_evaporation(self):
        tracker = self.commodity_tracker
        tracker.set_value(self.evaporation_data.commodity, self.evaporation_data.initial_value.random_float())
        if self._evaporate_callback_handle is not None:
            tracker.remove_listener(self._evaporate_callback_handle)
        threshold = sims4.math.Threshold(0.0, operator.le)
        self._evaporate_callback_handle = tracker.create_and_add_listener(self.evaporation_data.commodity, threshold, self.evaporate)

    def evaporate(self, stat_instance):
        if self.in_use:
            self.start_evaporation()
            return
        if self._evaporate_callback_handle is not None:
            self.commodity_tracker.remove_listener(self._evaporate_callback_handle)
            self._evaporate_callback_handle = None
        if self.is_on_natural_ground():
            defs_to_make = sims4.random.weighted_random_item([(self.evaporation_outcome.nothing, None), (self.evaporation_outcome.weeds, self.WEED_DEFINITIONS), (self.evaporation_outcome.plant, self.PLANT_DEFINITIONS)])
            if defs_to_make:
                def_to_make = random.choice(defs_to_make)
                obj_location = sims4.math.Location(sims4.math.Transform(self.position, sims4.random.random_orientation()), self.routing_surface)
                (result, _) = build_buy.test_location_for_object(None, def_to_make.id, obj_location, [self])
                if result:
                    obj = objects.system.create_object(def_to_make)
                    obj.opacity = 0
                    obj.location = self.location
                    obj.fade_in()
        self.destroy(self, cause='Puddle is evaporating.', fade_duration=ClientObjectMixin.FADE_DURATION)

    def load_object(self, object_data, **kwargs):
        super().load_object(object_data, **kwargs)
        self.start_evaporation()