Esempio n. 1
0
class RelationshipSimAffinityStrategy(TunableFactory):
    RELATIONSHIP_TO_ATTRACTION_BONUS_CURVE = TunableCurve(
        description=
        '\n        Tunable curve where the X-axis defines the relationship level between \n        two sims while the Y-axis defines the attraction bonus.\n        Note: Negative numbers are a penalty.\n        ',
        x_axis_name='Relationship',
        y_axis_name='Attraction Bonus')
    DISTANCE_TO_IMPACT_CURVE = TunableCurve(
        description=
        '\n        Tunable curve where the X-axis defines the distance between the goal\n        and the target while the y axis corresponds at the impact multiplier \n        that will affect how much of the relationship affinity should affect\n        the scoring of the goal.\n        Example:\n        If the sims are at a close distance less than 2 meters, we want to tune\n        the curve to be 1 from 0 to 2 meters, this way the impact of the\n        relationship will not make the sims reposition if they are already\n        close to each other.  Then after two meters we want to start decaying\n        how much impact the relationship should affect, so setting 0 impact on \n        the y axis at a distance of 10 meters will create a linear decay where\n        after 10 meters the relationship will not affect the goal scoring.\n        ',
        x_axis_name='Distance',
        y_axis_name='Impact Modifier')

    @staticmethod
    def _get_affinity(sim, other_sim):
        aggregate_track_value = [
            track.get_value() for track in
            sim.relationship_tracker.relationship_tracks_gen(other_sim.id)
            if track.is_scored
        ]
        average_track_value = sum(aggregate_track_value) / len(
            aggregate_track_value) if aggregate_track_value else 0
        sim_affinity = RelationshipSimAffinityStrategy.RELATIONSHIP_TO_ATTRACTION_BONUS_CURVE.get(
            average_track_value)
        return (-sim_affinity,
                'Sim Affinity: basic nearby Sim with relationship bonus: {}')

    FACTORY_TYPE = _get_affinity
Esempio n. 2
0
class SocialGeometry:
    __qualname__ = 'SocialGeometry'
    __slots__ = ('focus', 'field', '_area', 'transform')
    GROUP_DISTANCE_CURVE = TunableCurve(
        description=
        '\n    A curve defining the score for standing a given distance away from other\n    Sims in the social group.\n    \n    Higher values (on the y-axis) encourage standing at that distance (on the\n    x-axis) away from other Sims.'
    )
    NON_GROUP_DISTANCE_CURVE = TunableCurve(
        description=
        '\n    A curve defining the score for standing a given distance away from other\n    Sims *not* in the social group.\n    \n    Higher values (on the y-axis) encourage standing at that distance (on the\n     x-axis) away from other Sims.'
    )
    GROUP_ANGLE_CURVE = TunableCurve(
        description=
        '\n    A curve defining the score for two Sims with this facing angle (in radians).\n    \n    An angle of zero (on the x-axis) means a Sims is facing another Sim, while\n    PI means a Sim is facing away.  Higher values (on the y-axis) encourage\n    that angular facing.'
    )
    OVERLAP_SCORE_MULTIPLIER = Tunable(
        float,
        1.0,
        description=
        'p\n    Higher values raise the importance of the "personal space" component of the\n    social scoring function.'
    )
    DEFAULT_SCORE_CUTOFF = TunableRange(
        float,
        0.8,
        minimum=0,
        maximum=1.0,
        description=
        '\n    Transforms scoring below cutoff * max_score are filtered out when joining / adjusting position'
    )
    NON_OVERLAPPING_SCORE_MULTIPLIER = Tunable(
        float,
        0.05,
        description='Minimum score multiplier for non-overlapping fields')
    SCORE_STRENGTH_MULTIPLIER = Tunable(
        float,
        3,
        description=
        '\n    Values > 1 will cause Sims to go further out of their way to be in perfect social arrangements.\n    This helps overcome distance attenuation for social adjustment since we want Sims to care more\n    about where they are positioned than how far they have to go to improve that position.'
    )
    SCORE_OFFSET_FOR_CURRENT_POSITION = Tunable(
        float,
        0.5,
        description=
        "\n    An additional score to apply to points that are virtually identical to the\n    Sim's current position if the Sim already has an entry in the geometry.\n    \n    Larger numbers provide more friction that will prevent Sims from moving\n    away from their current position unless the score of the new point makes\n    moving worthwhile."
    )

    def __init__(self, focus, field, transform):
        self.focus = focus
        self.field = field
        self.transform = transform
        self._area = None

    def __repr__(self):
        return 'SocialGeometry[Focus:{}]'.format(self.focus)

    @property
    def area(self):
        if self._area is None:
            self._area = self.field.area()
        return self._area
Esempio n. 3
0
    class LamarkianInheritance(_Inheritance):
        __qualname__ = 'GardeningTuning.LamarkianInheritance'
        CALCULATIONS = {
            ParentalContribution.MAXIMUM: max,
            ParentalContribution.MINIMUM: min,
            ParentalContribution.AVERAGE: lambda l: sum(l) / len(l)
        }
        FACTORY_TUNABLES = {
            '_calculation':
            TunableEnumEntry(ParentalContribution,
                             ParentalContribution.MINIMUM,
                             needs_tuning=True),
            'fitness_stat':
            Statistic.TunableReference(
                description=
                '\n            This statistic is used as the x-value on the fitness curve.\n            '
            ),
            'fitness_curve_maximum':
            TunableCurve(
                description=
                '\n            This curve maps fitness stat values to maximum state changes.\n            '
            ),
            'fitness_curve_minimum':
            TunableCurve(
                description=
                '\n            This curve maps fitness stat values to minimum state changes.\n            '
            )
        }

        @property
        def calculation(self):
            return self.CALCULATIONS[self._calculation]

        def get_inherited_value(self, mother, father):
            values = []
            indeces = []
            if self.inherit_from_mother:
                values.append(mother.get_stat_value(self.fitness_stat))
                inherited_state_value = mother.get_state(self.inherited_state)
                indeces.append(
                    self.inherited_state.values.index(inherited_state_value))
            if self.inherit_from_father:
                values.append(father.get_stat_value(self.fitness_stat))
                inherited_state_value = father.get_state(self.inherited_state)
                indeces.append(
                    self.inherited_state.values.index(inherited_state_value))
            fitness_value = self.calculation(values)
            max_delta = self.fitness_curve_maximum.get(fitness_value)
            min_delta = self.fitness_curve_minimum.get(fitness_value)
            delta = round(sims4.random.uniform(min_delta, max_delta))
            index = self.calculation(indeces) + delta
            index = sims4.math.clamp(0, index,
                                     len(self.inherited_state.values))
            return self.inherited_state.values[index]
class SuccessChanceTuning:
    __qualname__ = 'SuccessChanceTuning'
    SCORE_CURVE = TunableCurve(
        verify_tunable_callback=_verify_tunable_callback,
        description=
        'A curve of score (X value) to percent chance of success (Y value). Percent chance should be in the range 0-1, while score is unbounded and may be negative.'
    )
class TunableStatisticModifierCurve(HasTunableSingletonFactory,
                                    AutoFactoryInit):
    __qualname__ = 'TunableStatisticModifierCurve'

    @TunableFactory.factory_option
    def axis_name_overrides(x_axis_name=None, y_axis_name=None):
        return {
            'multiplier':
            TunableVariant(
                description=
                '\n                Define how the multiplier will be applied.\n                ',
                value_curve=TunableCurve(
                    description=
                    '\n                    The multiplier will be determined by interpolating against a\n                    curve. The user-value is used. This means that a curve for\n                    skills should have levels as its x-axis.\n                    ',
                    x_axis_name=x_axis_name,
                    y_axis_name=y_axis_name),
                locked_args={'raw_value': None},
                default='raw_value')
        }

    FACTORY_TUNABLES = {
        'statistic':
        TunableReference(
            description=
            "\n            The payout amount will be multiplied by this statistic's value.\n            ",
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)),
        'subject':
        TunableEnumEntry(
            description=
            '\n            The participant to look for the specified statistic on.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'multiplier':
        TunableVariant(
            description=
            '\n            Define how the multiplier will be applied.\n            ',
            value_curve=TunableCurve(
                description=
                '\n                The multiplier will be determined by interpolating against a\n                curve. The user-value is used. This means that a curve for\n                skills should have levels as its x-axis.\n                '
            ),
            locked_args={'raw_value': None},
            default='raw_value')
    }

    def get_multiplier(self, resolver, sim):
        subject = resolver.get_participant(participant_type=self.subject,
                                           sim=sim)
        if subject is not None:
            stat = subject.get_stat_instance(self.statistic)
            if stat is not None:
                value = stat.convert_to_user_value(stat.get_value())
                if self.multiplier is not None:
                    return self.multiplier.get(value)
                return value
        return 1.0
class TunableObjectCostModifierCurve(HasTunableSingletonFactory, AutoFactoryInit):
	FACTORY_TUNABLES = {
		'subject': TunableEnumEntry(description = '\n            The object whose cost you want to base the multiplier on.\n            ', tunable_type = ParticipantTypeSingle, default = ParticipantTypeSingle.Object),
		'multiplier_curve': TunableCurve(description = ' \n            The multiplier will be determined by interpolating against a curve.\n            The value of the subject in simoleons is used. This means that a \n            curve for cost should have value at its x-axis.\n            ', x_axis_name = 'Value', y_axis_name = 'Multiplier')
	}

	def get_multiplier (self, resolver, sim):
		subject = resolver.get_participant(participant_type = self.subject, sim = sim)
		if subject is not None:
			value = subject.current_value
			return self.multiplier_curve.get(value)
		return 1.0
 def axis_name_overrides(x_axis_name=None, y_axis_name=None):
     return {
         'multiplier':
         TunableVariant(
             description=
             '\n                Define how the multiplier will be applied.\n                ',
             value_curve=TunableCurve(
                 description=
                 '\n                    The multiplier will be determined by interpolating against a\n                    curve. The user-value is used. This means that a curve for\n                    skills should have levels as its x-axis.\n                    ',
                 x_axis_name=x_axis_name,
                 y_axis_name=y_axis_name),
             locked_args={'raw_value': None},
             default='raw_value')
     }
Esempio n. 8
0
 def __init__(self, **kwargs):
     super().__init__(
         affordance_list=TunableList(
             description=
             '\n                List of affordances this multiplier will effect.\n                ',
             tunable=TunableReference(manager=services.affordance_manager(),
                                      reload_dependent=True)),
         curve=TunableCurve(
             description=
             '\n                Tunable curve where the X-axis defines the skill level, and\n                the Y-axis defines the associated multiplier.\n                ',
             x_axis_name='Skill Level',
             y_axis_name='Multiplier'),
         use_effective_skill=Tunable(
             description=
             '\n                If checked, this modifier will look at the current\n                effective skill value.  If unchecked, this modifier will\n                look at the actual skill value.\n                ',
             tunable_type=bool,
             needs_tuning=True,
             default=True),
         **kwargs)
Esempio n. 9
0
    class SkillTimeCurve(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {
            'skill':
            Skill.TunableReference(
                description=
                "\n                The skill that should influence the interaction's running time.\n                "
            ),
            'curve':
            TunableCurve(
                description=
                "\n                A curve describing the relationship between a Sim's skill level\n                (x-axis) and the interaction's running time (y-axis).  The time\n                is the number of Sim minutes it should take the specified goal\n                commodity to reach the goal value in the worst case, that is, if\n                the stat is as far from the goal value as possible.\n                "
            )
        }

        @property
        def stat(self):
            return self.skill

        def get_maximum_running_time(self, interaction):
            skill_level = interaction.sim.get_effective_skill_level(self.skill)
            time = self.curve.get(skill_level)
            return time
Esempio n. 10
0
class RelationshipSimAffinityStrategy(TunableFactory):
    __qualname__ = 'RelationshipSimAffinityStrategy'
    RELATIONSHIP_TO_ATTRACTION_BONUS_CURVE = TunableCurve(
        description=
        '\n        Tunable curve where the X-axis defines the relationship level between \n        two sims while the Y-axis defines the attraction bonus.\n        Note: Negative numbers are a penalty.\n        ',
        x_axis_name='Relationship',
        y_axis_name='Attraction Bonus')

    @staticmethod
    def _get_affinity(sim, other_sim):
        aggregate_track_value = [
            track.get_value() for track in
            sim.relationship_tracker.relationship_tracks_gen(other_sim.id)
            if track.is_scored
        ]
        average_track_value = sum(aggregate_track_value) / len(
            aggregate_track_value) if aggregate_track_value else 0
        sim_affinity = RelationshipSimAffinityStrategy.RELATIONSHIP_TO_ATTRACTION_BONUS_CURVE.get(
            average_track_value)
        return (-sim_affinity,
                'Sim Affinity: basic nearby Sim with relationship bonus: {}')

    FACTORY_TYPE = _get_affinity
Esempio n. 11
0
class BucksRecycling:
    RECYCLING_VALUE = TunableMapping(
        description=
        '\n        Maps a buck type the parameters controlling object recycling value.\n        Recycling Formula:\n        Total = Base Value * Price Response Curve (Object Current Simoleon Value) * \n            Object Recycle Value (Buck Type)\n        ',
        key_type=TunableEnumEntry(tunable_type=BucksType,
                                  default=BucksType.INVALID,
                                  invalid_enums=BucksType.INVALID,
                                  pack_safe=True),
        key_name='Bucks Type',
        value_type=TunableTuple(
            description=
            '\n            Recycling parameters for this buck type.\n            ',
            base_value=TunableRange(
                description=
                '\n                Base multiplier for this buck type\n                ',
                tunable_type=float,
                default=1.0,
                minimum=0.0),
            price_response_curve=TunableCurve(
                description=
                '\n                Modulate the base value by the objects Simoleon value.\n                ',
                x_axis_name='Object Price',
                y_axis_name='Base Multiplier')),
        value_name='Recycled Value')

    @classmethod
    def get_recycling_value_for_object(cls, bucks, obj):
        if obj is None or (obj.is_sim
                           or bucks not in obj.recycling_data.recycling_values
                           ) or bucks not in BucksRecycling.RECYCLING_VALUE:
            return 0
        object_recycle_value = obj.recycling_data.recycling_values[bucks]
        object_value = obj.current_value
        params = BucksRecycling.RECYCLING_VALUE[bucks]
        recycle_curve_value = params.price_response_curve.get(object_value)
        return int(params.base_value * recycle_curve_value *
                   object_recycle_value * object_value)
Esempio n. 12
0
class Skill(HasTunableReference,
            statistics.continuous_statistic_tuning.TunedContinuousStatistic,
            metaclass=HashedTunedInstanceMetaclass,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)):
    __qualname__ = 'Skill'
    SKILL_LEVEL_LIST = TunableMapping(
        key_type=TunableEnumEntry(SkillLevelType, SkillLevelType.MAJOR),
        value_type=TunableList(
            Tunable(int, 0),
            description=
            'The level boundaries for skill type, specified as a delta from the previous value'
        ),
        export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(
        key_type=TunableEnumEntry(SkillEffectiveness,
                                  SkillEffectiveness.STANDARD),
        value_type=TunableCurve(),
        description='Skill gain points based on skill effectiveness.')
    DYNAMIC_SKILL_INTERVAL = TunableRange(
        description=
        '\n        Interval used when dynamic loot is used in a\n        PeriodicStatisticChangeElement.\n        ',
        tunable_type=float,
        default=1,
        minimum=1)
    INSTANCE_TUNABLES = {
        'stat_name':
        TunableLocalizedString(
            description=
            '\n            Localized name of this Statistic\n            ',
            export_modes=ExportModes.All),
        'ad_data':
        TunableList(
            description=
            '\n            A list of Vector2 points that define the desire curve for this\n            commodity.\n            ',
            tunable=TunableVector2(
                description=
                '\n                Point on a Curve\n                ',
                default=sims4.math.Vector2(0, 0))),
        'weight':
        Tunable(
            description=
            "\n            The weight of the Skill with regards to autonomy.  It's ignored \n            for the purposes of sorting stats, but it's applied when scoring \n            the actual statistic operation for the SI.\n            ",
            tunable_type=float,
            default=0.5),
        'skill_level_type':
        TunableEnumEntry(
            description='\n            Skill level list to use.\n            ',
            tunable_type=SkillLevelType,
            default=SkillLevelType.MAJOR,
            export_modes=ExportModes.All),
        'locked_description':
        TunableLocalizedString(
            description=
            "\n            The skill description when it's locked.\n            ",
            export_modes=ExportModes.All),
        'skill_description':
        TunableLocalizedString(
            description=
            "\n            The skill's normal description.\n            ",
            export_modes=ExportModes.All),
        'is_default':
        Tunable(
            description=
            '\n            Whether Sim will default has this skill.\n            ',
            tunable_type=bool,
            default=False),
        'genders':
        TunableSet(
            description=
            '\n            Skill allowed gender, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Gender,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'ages':
        TunableSet(
            description=
            '\n            Skill allowed ages, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Age,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'entitlement':
        TunableEntitlement(
            description=
            '\n            Entitlement required to use this skill.\n            '
        ),
        'icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed for the Skill.\n            ',
            default='PNG:missing_image',
            resource_types=sims4.resources.CompoundTypes.IMAGE,
            export_modes=ExportModes.All),
        'tags':
        TunableList(
            description=
            '\n            The associated categories of the skill\n            ',
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID)),
        'priority':
        Tunable(
            description=
            '\n            Skill priority.  Higher priority skill will trump other skills when\n            being displayed on the UI side. When a sim gains multiple skills at\n            the same time only the highest priority one will display a progress\n            bar over its head.\n            ',
            tunable_type=int,
            default=1,
            export_modes=ExportModes.All),
        'statistic_multipliers':
        TunableMapping(
            description=
            '\n            Multipliers this skill applies to other statistics based on its\n            value.\n            ',
            key_type=TunableReference(
                description=
                '\n                The statistic this multiplier will be applied to.\n                ',
                manager=services.statistic_manager(),
                reload_dependent=True),
            value_type=TunableTuple(
                curve=TunableCurve(
                    description=
                    '\n                    Tunable curve where the X-axis defines the skill level, and\n                    the Y-axis defines the associated multiplier.\n                    ',
                    x_axis_name='Skill Level',
                    y_axis_name='Multiplier'),
                direction=TunableEnumEntry(
                    description=
                    "\n                    Direction where the multiplier should work on the\n                    statistic.  For example, a tuned decrease for an object's\n                    brokenness rate will not also increase the time it takes to\n                    repair it.\n                    ",
                    tunable_type=StatisticChangeDirection,
                    default=StatisticChangeDirection.INCREASE),
                use_effective_skill=Tunable(
                    description=
                    '\n                    If checked, this modifier will look at the current\n                    effective skill value.  If unchecked, this modifier will\n                    look at the actual skill value.\n                    ',
                    tunable_type=bool,
                    needs_tuning=True,
                    default=True)),
            tuning_group=GroupNames.MULTIPLIERS),
        'success_chance_multipliers':
        TunableList(
            description=
            '\n            Multipliers this skill applies to the success chance of\n            affordances.\n            ',
            tunable=TunableSkillMultiplier(),
            tuning_group=GroupNames.MULTIPLIERS),
        'monetary_payout_multipliers':
        TunableList(
            description=
            '\n            Multipliers this skill applies to the monetary payout amount of\n            affordances.\n            ',
            tunable=TunableSkillMultiplier(),
            tuning_group=GroupNames.MULTIPLIERS),
        'next_level_teaser':
        TunableList(
            description=
            '\n            Tooltip which describes what the next level entails.\n            ',
            tunable=TunableLocalizedString(),
            export_modes=(ExportModes.ClientBinary, )),
        'level_data':
        TunableMapping(
            description=
            '\n            Level-specific information, such as notifications to be displayed to\n            level up.\n            ',
            key_type=int,
            value_type=TunableTuple(
                level_up_notification=UiDialogNotification.TunableFactory(
                    description=
                    '\n                    The notification to display when the Sim obtains this level.\n                    The text will be provided two tokens: the Sim owning the\n                    skill and a number representing the 1-based skill level\n                    ',
                    locked_args={
                        'text_tokens':
                        DEFAULT,
                        'icon':
                        None,
                        'primary_icon_response':
                        UiDialogResponse(text=None,
                                         ui_request=UiDialogResponse.
                                         UiDialogUiRequest.SHOW_SKILL_PANEL),
                        'secondary_icon':
                        None
                    }),
                level_up_screen_slam=OptionalTunable(
                    description=
                    '\n                    Screen slam to show when reaches this skill level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n                    {1.String}, Skill Number - {2.Number}\n                    ',
                    tunable=ui.screen_slam.TunableScreenSlamSnippet(),
                    tuning_group=GroupNames.UI))),
        'mood_id':
        TunableReference(
            description=
            '\n            When this mood is set and active sim matches mood, the UI will \n            display a special effect on the skill bar to represent that this \n            skill is getting a bonus because of the mood.\n            ',
            manager=services.mood_manager(),
            export_modes=ExportModes.All),
        'stat_asm_param':
        TunableStatAsmParam.TunableFactory(),
        'tutorial':
        TunableReference(
            description=
            '\n            Tutorial instance for this skill. This will be used to bring up the \n            skill lesson from the first notification for Sim to know this skill.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.TUTORIAL),
            class_restrictions=('Tutorial', )),
        'skill_unlocks_on_max':
        TunableList(
            description=
            '\n            A list of skills that become unlocked when this skill is maxed.\n            ',
            tunable=TunableReference(
                description=
                '\n                A skill to unlock.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.STATISTIC),
                class_restrictions=('Skill', )))
    }
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning',
                                'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._callback_handle = None
        if self.tracker.owner.is_simulating:
            self.on_initial_startup()
        self._max_level_update_sent = False

    def on_initial_startup(self):
        if self.tracker.owner.is_selectable:
            self.refresh_level_up_callback()

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()

    def _apply_multipliers_to_continuous_statistics(self):
        for stat in self.statistic_multipliers:
            while stat.continuous:
                owner_stat = self.tracker.get_statistic(stat)
                if owner_stat is not None:
                    owner_stat._recalculate_modified_decay_rate()

    @caches.cached
    def get_user_value(self):
        return super(Skill, self).get_user_value()

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        self.get_user_value.cache.clear()
        if not from_load:
            new_value = self.get_value()
            new_level = self.convert_to_user_value(value)
            if old_value == self.initial_value and old_value != new_value:
                sim_info = self._tracker._owner
                services.get_event_manager().process_event(
                    test_events.TestEvent.SkillLevelChange,
                    sim_info=sim_info,
                    statistic=self.stat_type)
            old_level = self.convert_to_user_value(old_value)
            if old_level < new_level:
                self._apply_multipliers_to_continuous_statistics()

    def add_value(self, add_amount, interaction=None, **kwargs):
        old_value = self.get_value()
        if old_value == self.initial_value:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
        else:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION
        super().add_value(add_amount, interaction=interaction)
        self.get_user_value.cache.clear()
        if interaction is not None:
            self.on_skill_updated(telemhook, old_value, self.get_value(),
                                  interaction.affordance.__name__)

    def _update_value(self):
        old_value = self._value
        if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled:
            last_update = self._last_update
        time_delta = super()._update_value()
        self.get_user_value.cache.clear()
        new_value = self._value
        if old_value == self.initial_value:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
            self.on_skill_updated(telemhook, old_value, new_value,
                                  TELEMETRY_INTERACTION_NOT_AVAILABLE)
            sim_info = self._tracker._owner
            services.get_event_manager().process_event(
                test_events.TestEvent.SkillLevelChange,
                sim_info=sim_info,
                statistic=self.stat_type)
        old_level = self.convert_to_user_value(old_value)
        new_level = self.convert_to_user_value(new_value)
        if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim:
            gsi_handlers.sim_handlers_log.archive_skill_change(
                self.tracker.owner, self, time_delta, old_value, new_value,
                new_level, last_update)
        if old_value < new_value and old_level < new_level:
            if self._tracker is not None:
                self._tracker.notify_watchers(self.stat_type, self._value,
                                              self._value)

    def on_skill_updated(self, telemhook, old_value, new_value,
                         affordance_name):
        owner_sim = self._tracker._owner
        if owner_sim.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer,
                                             telemhook,
                                             sim=owner_sim) as hook:
                hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
                hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE,
                                  affordance_name)
                hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True)
                hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD,
                               new_value - old_value)
        if old_value == self.initial_value:
            skill_level = self.convert_to_user_value(old_value)
            self._show_level_notification(skill_level)

    def _destory_callback_handle(self):
        if self._callback_handle is not None:
            self.remove_callback(self._callback_handle)
            self._callback_handle = None

    def refresh_level_up_callback(self):
        self._destory_callback_handle()

        def _on_level_up_callback(stat_inst):
            new_level = stat_inst.get_user_value()
            old_level = new_level - 1
            stat_inst.on_skill_level_up(old_level, new_level)
            stat_inst.refresh_level_up_callback()

        self._callback_handle = self.add_callback(
            Threshold(self._get_next_level_bound(), operator.ge),
            _on_level_up_callback)

    def on_skill_level_up(self, old_level, new_level):
        tracker = self.tracker
        sim_info = tracker._owner
        if self.reached_max_level:
            for skill in self.skill_unlocks_on_max:
                skill_instance = tracker.add_statistic(skill, force_add=True)
                skill_instance.set_value(skill.initial_value)
        with telemetry_helper.begin_hook(skill_telemetry_writer,
                                         TELEMETRY_HOOK_SKILL_LEVEL_UP,
                                         sim=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        if sim_info.account is not None:
            services.social_service.post_skill_message(sim_info, self,
                                                       old_level, new_level)
        self._show_level_notification(new_level)
        services.get_event_manager().process_event(
            test_events.TestEvent.SkillLevelChange,
            sim_info=sim_info,
            statistic=self.stat_type)

    def _show_level_notification(self, skill_level):
        sim_info = self._tracker._owner
        if not sim_info.is_npc:
            level_data = self.level_data.get(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None and skill_level == 1:
                    tutorial_id = self.tutorial.guid64
                notification = level_data.level_up_notification(
                    sim_info, resolver=SingleSimResolver(sim_info))
                notification.show_dialog(icon_override=(self.icon, None),
                                         secondary_icon_override=(None,
                                                                  sim_info),
                                         additional_tokens=(skill_level, ),
                                         tutorial_id=tutorial_id)
                if level_data.level_up_screen_slam is not None:
                    level_data.level_up_screen_slam.send_screen_slam_message(
                        sim_info, sim_info, self.stat_name, skill_level)

    @classproperty
    def skill_type(cls):
        return cls

    @classproperty
    def remove_on_convergence(cls):
        return False

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if cls.genders and owner.gender not in cls.genders:
            return False
        if cls.ages and owner.age not in cls.ages:
            return False
        if cls.entitlement is None:
            return True
        if owner.is_npc:
            return False
        return mtx.has_entitlement(cls.entitlement)

    @classmethod
    def get_level_list(cls):
        return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type)

    @classmethod
    def get_max_skill_value(cls):
        level_list = cls.get_level_list()
        return sum(level_list)

    @classmethod
    def get_skill_value_for_level(cls, level):
        level_list = cls.get_level_list()
        if level > len(level_list):
            logger.error('Level {} out of bounds', level)
            return 0
        return sum(level_list[:level])

    @classmethod
    def get_skill_effectiveness_points_gain(cls, effectiveness_level, level):
        skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(
            effectiveness_level)
        if skill_gain_curve is not None:
            return skill_gain_curve.get(level)
        logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping',
                     effectiveness_level)
        return 0

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        level_list = cls.get_level_list()
        cls.max_level = len(level_list)
        cls.min_value_tuning = 0
        cls.max_value_tuning = sum(level_list)
        cls._default_convergence_value = cls.min_value_tuning
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        for stat in cls.statistic_multipliers:
            multiplier = cls.statistic_multipliers[stat]
            curve = multiplier.curve
            direction = multiplier.direction
            use_effective_skill = multiplier.use_effective_skill
            stat.add_skill_based_statistic_multiplier(cls, curve, direction,
                                                      use_effective_skill)
        for multiplier in cls.success_chance_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(
                    affordance.success_chance_multipliers, cls, curve,
                    use_effective_skill)
        for multiplier in cls.monetary_payout_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(
                    affordance.monetary_payout_multipliers, cls, curve,
                    use_effective_skill)

    @classmethod
    def _verify_tuning_callback(cls):
        success_multiplier_affordances = []
        for multiplier in cls.success_chance_multipliers:
            success_multiplier_affordances.extend(multiplier.affordance_list)
        if len(success_multiplier_affordances) != len(
                set(success_multiplier_affordances)):
            logger.error(
                "The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.",
                cls,
                owner='tastle')
        monetary_payout_multiplier_affordances = []
        for multiplier in cls.monetary_payout_multipliers:
            monetary_payout_multiplier_affordances.extend(
                multiplier.affordance_list)
        if len(monetary_payout_multiplier_affordances) != len(
                set(monetary_payout_multiplier_affordances)):
            logger.error(
                "The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.",
                cls,
                owner='tastle')

    @classmethod
    def convert_to_user_value(cls, value):
        if not cls.get_level_list():
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(cls.get_level_list()):
            current_value -= level_threshold
            while current_value < 0:
                return level
        return level + 1

    @classmethod
    def convert_from_user_value(cls, user_value):
        (level_min, _) = cls._get_level_bounds(user_value)
        return level_min

    @classmethod
    def _get_level_bounds(cls, level):
        level_list = cls.get_level_list()
        level_min = sum(level_list[:level])
        if level < cls.max_level:
            level_max = sum(level_list[:level + 1])
        else:
            level_max = sum(level_list)
        return (level_min, level_max)

    def _get_next_level_bound(self):
        level = self.convert_to_user_value(self._value)
        (_, level_max) = self._get_level_bounds(level)
        return level_max

    @property
    def reached_max_level(self):
        max_value = self.get_max_skill_value()
        if self.get_value() >= max_value:
            return True
        return False

    @property
    def should_send_update(self):
        if not self.reached_max_level:
            return True
        if not self._max_level_update_sent:
            self._max_level_update_sent = True
            return True
        return False

    @classproperty
    def is_skill(cls):
        return True

    @classproperty
    def autonomy_weight(cls):
        return cls.weight

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        if not cls.convert_to_user_value(stat_value) > 0:
            return
        skill_msg = Commodities_pb2.Skill_Update()
        skill_msg.skill_id = cls.guid64
        skill_msg.curr_points = int(stat_value)
        skill_msg.sim_id = sim_id
        return skill_msg

    @property
    def is_initial_value(self):
        return self.initial_value == self.get_value()

    @classproperty
    def valid_for_stat_testing(cls):
        return True
class RetailCustomerSituation(BusinessSituationMixin, SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'customer_job':
        SituationJob.TunableReference(
            description=
            '\n            The situation job for the customer.\n            '),
        'role_state_go_to_store':
        RoleState.TunableReference(
            description=
            '\n            The role state for getting the customer inside the store. This is\n            the default role state and will be run first before any other role\n            state can start.\n            '
        ),
        'role_state_browse':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to browse items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer browsing items.\n                    '
                ),
                browse_time_min=TunableSimMinute(
                    description=
                    '\n                    The minimum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=10),
                browse_time_max=TunableSimMinute(
                    description=
                    '\n                    The maximum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=20),
                browse_time_extension_tunables=OptionalTunable(
                    TunableTuple(
                        description=
                        '\n                    A set of tunables related to browse time extensions.\n                    ',
                        extension_perk=TunableReference(
                            description=
                            '\n                        Reference to a perk that, if unlocked, will increase\n                        browse time by a set amount.\n                        ',
                            manager=services.get_instance_manager(
                                sims4.resources.Types.BUCKS_PERK)),
                        time_extension=TunableSimMinute(
                            description=
                            '\n                        The amount of time, in Sim minutes, that browse time\n                        will be increased by if the specified "extension_perk"\n                        is unlocked.\n                        ',
                            default=30))))),
        'role_state_buy':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to buy items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer buying items.\n                    '
                ),
                price_range=TunableInterval(
                    description=
                    '\n                    The minimum and maximum price of items this customer will\n                    buy.\n                    ',
                    tunable_type=int,
                    default_lower=1,
                    default_upper=100,
                    minimum=1))),
        'role_state_loiter':
        RoleState.TunableReference(
            description=
            '\n            The role state for the customer loitering. If Buy Role State and\n            Browse Role State are both disabled, the Sim will fall back to\n            loitering until Total Shop Time runs out.\n            '
        ),
        'go_to_store_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, will switch the\n            situation state to start browsing, buying, or loitering.\n            '
        ),
        'total_shop_time_max':
        TunableSimMinute(
            description=
            "\n            The maximum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=30),
        'total_shop_time_min':
        TunableSimMinute(
            description=
            "\n            The minimum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=1),
        'buy_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, buys an object.\n            '
        ),
        'initial_purchase_intent':
        TunableInterval(
            description=
            "\n            The customer's purchase intent statistic is initialized to a random\n            value in this interval when they enter the store.\n            ",
            tunable_type=int,
            default_lower=0,
            default_upper=100),
        'purchase_intent_extension_tunables':
        OptionalTunable(
            TunableTuple(
                description=
                '\n            A set of tunables related to purchase intent extensions.\n            ',
                extension_perk=TunableReference(
                    description=
                    '\n                Reference to a perk that, if unlocked, will increase purchase\n                intent by a set amount.\n                ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.BUCKS_PERK)),
                purchase_intent_extension=TunableRange(
                    description=
                    '\n                The amount to increase the base purchase intent statistic by if\n                the specified "extension_perk" is unlocked.\n                ',
                    tunable_type=int,
                    default=5,
                    minimum=0,
                    maximum=100))),
        'purchase_intent_empty_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            Notification shown by customer when purchase intent hits bottom and\n            the customer leaves.\n            '
        ),
        'nothing_in_price_range_notification':
        TunableUiDialogNotificationSnippet(
            description=
            "\n            Notification shown by customers who are ready to buy but can't find\n            anything in their price range.\n            "
        ),
        '_situation_start_tests':
        TunableCustomerSituationInitiationSet(
            description=
            '\n            A set of tests that will be run when determining if this situation\n            can be chosen to start. \n            '
        )
    }
    CONTINUE_SHOPPING_THRESHOLD = TunableSimMinute(
        description=
        "\n        If the customer has this much time or more left in their total shop\n        time, they'll start the browse/buy process over again after purchasing\n        something. If they don't have this much time remaining, they'll quit\n        shopping.\n        ",
        default=30)
    PRICE_RANGE = TunableTuple(
        description=
        '\n        Statistics that are set to the min and max price range statistics.\n        These are automatically added to the customer in this situation and\n        will be updated accordingly.\n        \n        The stats should not be persisted -- the situation will readd them\n        on load.\n        ',
        min=Statistic.TunablePackSafeReference(),
        max=Statistic.TunablePackSafeReference())
    PURCHASE_INTENT_STATISTIC = Statistic.TunablePackSafeReference(
        description=
        "\n        A statistic added to customers that track their intent to purchase\n        something. At the minimum value they will leave, and at max value they\n        will immediately try to buy something. Somewhere in between, there's a\n        chance for them to not buy something when they go to the buy state.\n        "
    )
    PURCHASE_INTENT_CHANCE_CURVE = TunableCurve(
        description=
        '\n        A mapping of Purchase Intent Statistic value to the chance (0-1) that\n        the customer will buy something during the buy state.\n        ',
        x_axis_name='Purchase Intent',
        y_axis_name='Chance')
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def can_start_situation(cls, resolver):
        return cls._situation_start_tests.run_tests(resolver)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._customer = None
        self._showing_purchase_intent = False
        reader = self._seed.custom_init_params_reader
        if reader is None:
            self._saved_purchase_intent = None
        else:
            self._saved_purchase_intent = reader.read_int64(
                'purchase_intent', None)
        self._min_price_range_multiplier = 1
        self._max_price_range_multiplier = 1
        self._total_shop_time_multiplier = 1
        self._purchase_intent_watcher_handle = None

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        if self._customer is not None:
            purchase_intent = self._customer.get_stat_value(
                self.PURCHASE_INTENT_STATISTIC)
            writer.write_int64('purchase_intent', int(purchase_intent))

    @classmethod
    def _states(cls):
        return (SituationStateData(1, _GoToStoreState),
                SituationStateData(2, _BrowseState),
                SituationStateData(3, _BuyState),
                SituationStateData(4, _LoiterState))

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.customer_job, cls.role_state_go_to_store)]

    @classmethod
    def default_job(cls):
        return cls.customer_job

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

    @classmethod
    def get_sims_expected_to_be_in_situation(cls):
        return 1

    @classproperty
    def situation_serialization_option(cls):
        return situations.situation_types.SituationSerializationOption.LOT

    def validate_customer(self, sim_info):
        if self._customer is None:
            return False
        return self._customer.sim_info is sim_info

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        self._customer = sim
        self._update_price_range_statistics()
        self._initialize_purchase_intent()

    def _on_remove_sim_from_situation(self, sim):
        sim_job = self.get_current_job_for_sim(sim)
        super()._on_remove_sim_from_situation(sim)
        self._remove_purchase_intent()
        self._customer = None
        services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist(
            sim.id, sim_job)
        self._self_destruct()

    def _situation_timed_out(self, *args, **kwargs):
        if not isinstance(self._cur_state, _BuyState):
            super()._situation_timed_out(*args, **kwargs)

    def adjust_browse_time(self, multiplier):
        if type(self._cur_state) is _BrowseState:
            self._cur_state.adjust_timeout(multiplier)

    def adjust_total_shop_time(self, multiplier):
        if multiplier == 0:
            self._self_destruct()
        elif type(self._cur_state) is _GoToStoreState:
            self._total_shop_time_multiplier *= multiplier
        else:
            remaining_minutes = self._get_remaining_time_in_minutes()
            remaining_minutes *= multiplier
            self.change_duration(remaining_minutes)

    def adjust_price_range(self, min_multiplier=1, max_multiplier=1):
        if self.role_state_buy is None:
            return
        self._min_price_range_multiplier *= min_multiplier
        self._max_price_range_multiplier *= max_multiplier
        self._update_price_range_statistics()

    def _update_price_range_statistics(self):
        (min_price, max_price) = self._get_min_max_price_range()
        if self.PRICE_RANGE.min is not None:
            min_stat = self._customer.get_statistic(self.PRICE_RANGE.min)
            min_stat.set_value(min_price)
        if self.PRICE_RANGE.max is not None:
            max_stat = self._customer.get_statistic(self.PRICE_RANGE.max)
            max_stat.set_value(max_price)

    def _get_min_max_price_range(self):
        price_range = self.role_state_buy.price_range
        return (max(0, price_range.lower_bound *
                    self._min_price_range_multiplier),
                max(1, price_range.upper_bound *
                    self._max_price_range_multiplier))

    def _initialize_purchase_intent(self):
        if self.role_state_buy is None:
            return
        if self._saved_purchase_intent is None:
            purchase_intent = random.randint(
                self.initial_purchase_intent.lower_bound,
                self.initial_purchase_intent.upper_bound)
            if self.purchase_intent_extension_tunables is not None:
                active_household = services.active_household()
                if active_household is not None:
                    if active_household.bucks_tracker.is_perk_unlocked(
                            self.purchase_intent_extension_tunables.
                            extension_perk):
                        purchase_intent += self.purchase_intent_extension_tunables.purchase_intent_extension
            purchase_intent = sims4.math.clamp(
                self.PURCHASE_INTENT_STATISTIC.min_value + 1, purchase_intent,
                self.PURCHASE_INTENT_STATISTIC.max_value - 1)
        else:
            purchase_intent = self._saved_purchase_intent
        tracker = self._customer.get_tracker(self.PURCHASE_INTENT_STATISTIC)
        tracker.set_value(self.PURCHASE_INTENT_STATISTIC,
                          purchase_intent,
                          add=True)
        self._purchase_intent_watcher_handle = tracker.add_watcher(
            self._purchase_intent_watcher)
        if self._on_social_group_changed not in self._customer.on_social_group_changed:
            self._customer.on_social_group_changed.append(
                self._on_social_group_changed)

    def _remove_purchase_intent(self):
        if self._customer is not None:
            if self._purchase_intent_watcher_handle is not None:
                tracker = self._customer.get_tracker(
                    self.PURCHASE_INTENT_STATISTIC)
                tracker.remove_watcher(self._purchase_intent_watcher_handle)
                self._purchase_intent_watcher_handle = None
                tracker.remove_statistic(self.PURCHASE_INTENT_STATISTIC)
            if self._on_social_group_changed in self._customer.on_social_group_changed:
                self._customer.on_social_group_changed.remove(
                    self._on_social_group_changed)
            self._set_purchase_intent_visibility(False)

    def _on_social_group_changed(self, sim, group):
        if self._customer in group:
            if self._on_social_group_members_changed not in group.on_group_changed:
                group.on_group_changed.append(
                    self._on_social_group_members_changed)
        elif self._on_social_group_members_changed in group.on_group_changed:
            group.on_group_changed.remove(
                self._on_social_group_members_changed)

    def _on_social_group_members_changed(self, group):
        if self._customer is not None:
            employee_still_in_group = False
            business_manager = services.business_service(
            ).get_business_manager_for_zone()
            if self._customer in group:
                for sim in group:
                    if not business_manager.is_household_owner(
                            sim.household_id):
                        if business_manager.is_employee(sim.sim_info):
                            employee_still_in_group = True
                            break
                    employee_still_in_group = True
                    break
            if employee_still_in_group:
                self._set_purchase_intent_visibility(True)
            else:
                self._set_purchase_intent_visibility(False)

    def on_sim_reset(self, sim):
        super().on_sim_reset(sim)
        if isinstance(self._cur_state, _BuyState) and self._customer is sim:
            new_buy_state = _BuyState()
            new_buy_state.object_id = self._cur_state.object_id
            self._change_state(new_buy_state)

    def _set_purchase_intent_visibility(self, toggle):
        if self._showing_purchase_intent is not toggle and (
                not toggle or isinstance(self._cur_state, _BrowseState)):
            self._showing_purchase_intent = toggle
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                self._send_purchase_intent_message(stat.stat_type, value,
                                                   value, toggle)

    def _purchase_intent_watcher(self, stat_type, old_value, new_value):
        if stat_type is not self.PURCHASE_INTENT_STATISTIC:
            return
        self._send_purchase_intent_message(stat_type, old_value, new_value,
                                           self._showing_purchase_intent)
        if new_value == self.PURCHASE_INTENT_STATISTIC.max_value:
            self._on_purchase_intent_max()
        elif new_value == self.PURCHASE_INTENT_STATISTIC.min_value:
            self._on_purchase_intent_min()

    def _send_purchase_intent_message(self, stat_type, old_value, new_value,
                                      toggle):
        business_manager = services.business_service(
        ).get_business_manager_for_zone()
        if business_manager is not None and business_manager.is_owner_household_active:
            op = PurchaseIntentUpdate(
                self._customer.sim_id,
                stat_type.convert_to_normalized_value(old_value),
                stat_type.convert_to_normalized_value(new_value), toggle)
            distributor.system.Distributor.instance().add_op(
                self._customer, op)

    def _on_purchase_intent_max(self):
        if isinstance(self._cur_state, _BuyState):
            return
        if isinstance(self._cur_state, _GoToStoreState):
            self._set_shop_duration()
        self._change_state(_BuyState())

    def _on_purchase_intent_min(self):
        resolver = SingleSimResolver(self._customer)
        dialog = self.purchase_intent_empty_notification(
            self._customer, resolver)
        dialog.show_dialog()
        self._self_destruct()

    def _choose_starting_state(self):
        if self.role_state_browse is not None:
            return _BrowseState()
        if self.role_state_buy is not None:
            return _BuyState()
        return _LoiterState()

    def _choose_post_browse_state(self):
        if self._customer is None:
            return
        if self.role_state_buy is not None:
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                chance = self.PURCHASE_INTENT_CHANCE_CURVE.get(value)
                if random.random() > chance:
                    return _BrowseState()
            self._set_purchase_intent_visibility(False)
            return _BuyState()
        return _LoiterState()

    def _choose_post_buy_state(self):
        minutes_remaining = self._get_remaining_time_in_minutes()
        if minutes_remaining < self.CONTINUE_SHOPPING_THRESHOLD:
            return
        if self.role_state_browse is not None:
            return _BrowseState()
        return _LoiterState()

    def _set_shop_duration(self):
        shop_time = random.randint(self.total_shop_time_min,
                                   self.total_shop_time_max)
        shop_time *= self._total_shop_time_multiplier
        self.change_duration(shop_time)
Esempio n. 14
0
class CullingTuning:
    CULLING_GHOST_COMMODITY = TunableReference(
        description=
        "\n        The commodity that defines the strength of a ghost's connection to the\n        physical world. Sims with low values are close to being culled.\n        \n        This commodity being low also unlocks the ability for the player to run\n        certain interactions that strengthens a ghost's connection to the\n        physical world.\n        ",
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))
    CULLING_GHOST_WARNING_THRESHOLD = Tunable(
        description=
        "\n        Once a ghost's culling commodity is below this threshold, a warning\n        notification is displayed.\n        ",
        tunable_type=float,
        default=0)
    CULLING_GHOST_WARNING_NOTIFICATION = TunableUiDialogNotificationReference(
        description=
        "\n        This is a notification that is displayed whenever a ghost's culling\n        commodity reaches the threshold defined by\n        CULLING_GHOST_WARNING_THRESHOLD.\n        "
    )
    CULLING_OFFLOT_URNSTONE_PLACEMENT = PlacementHelper.TunableFactory(
        description=
        "\n        When Sims die off-lot, we attempt to place an urnstone at their old\n        household's home lot, upon travel. Define how urnstones should be\n        automatically placed.\n        "
    )
    CULLING_SCORE_IN_WORLD = TunableRange(
        description=
        '\n        A bonus score that is applied to all Sims living in the world. This\n        makes them less likely to be culled.\n        ',
        tunable_type=float,
        minimum=0,
        default=10)
    CULLING_SCORE_PREMADE = TunableRange(
        description=
        '\n        A bonus score that is applied to all Sims that are premade. This makes\n        them less likely to be culled.\n        ',
        tunable_type=float,
        minimum=0,
        default=10)
    CULLING_NOTIFICATION_IN_WORLD = TunableUiDialogNotificationReference(
        description=
        '\n        This is a flavor notification that is displayed whenever a non-player\n        household living in the world is culled, as long as this household has a\n        meaningful relationship with the active household.\n        '
    )
    RELATIONHSIP_DEPTH_WEIGHT = Tunable(
        description=
        '\n        Multiplier used to modify relationship depth to determine how\n        important depth is in culling score.  The higher the multiplier the\n        more relationship depth is added to culling score.  The lower the\n        culling score the more likely sim has a chance of being deleted.\n        ',
        tunable_type=float,
        default=0.5)
    RELATIONSHIP_TRACKS_MULTIPLIER = Tunable(
        description=
        '\n        Multiply the number of tracks by this multiplier to provide an\n        additional score to determine if sim should be culled. The higher\n        the multiplier the more the number of tracks bonus is added to\n        culling score.  The lower the culling score the more likely sim has\n        a chance of being deleted.\n        ',
        tunable_type=float,
        default=2)
    RELATIONSHIP_INSTANTIATION_TIME_CURVE = TunableCurve(
        description=
        "\n        Define a relationship score modifier based on the time since the\n        relationship target Sim was instantiated. The idea is that\n        relationships with Sims that haven't been instantiated recently\n        should count less.\n        ",
        x_axis_name='Days_Since_Instantiation',
        y_axis_name='Score_Multiplier')
    LAST_INSTANTIATED_MAX = TunableRange(
        description=
        '\n        Number of days before "last time instantiated" is no longer\n        considered for culling.\n        \n        Example: if set to 10, after 10 sim days only relationship depth\n        and track are considered when scoring sim for culling.\n        ',
        tunable_type=float,
        default=30,
        minimum=1)
    LAST_INSTANTIATED_WEIGHT = Tunable(
        description=
        '\n        Multiplier used to modify since "last time instantiated" to\n        determine how important depth is in culling score.\n        ',
        tunable_type=float,
        default=0.5)
    FAME_CULLING_BONUS_CURVE = TunableCurve(
        description=
        '\n        A curve specifying the culling bonus that a Sim receives at a given\n        Fame rank.\n        ',
        x_axis_name='Rank',
        y_axis_name='Bonus')
Esempio n. 15
0
class _AmbientSourceGhost(_AmbientSource):
    GHOST_SITUATIONS = TunableTestedList(
        description=
        '\n        A list of possible ghost situations, tested aginst the Sim we want to\n        spawn.\n        ',
        tunable_type=TunableReference(
            description=
            '\n            The ghost situation to spawn.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.SITUATION),
            pack_safe=True))
    DESIRED_GHOST_COUNT_PER_URNSTONE = TunableCurve(
        description=
        '\n        This curve describes the maximum number of ghosts we want in the world\n        based on the number of valid urnstones in the world. If there are more\n        urnstones than the maximum number tuned on the X axis, we will just use\n        the final Y value.\n        ',
        x_axis_name='Valid Urnstones',
        y_axis_name='Desired Ghost Count')
    WALKBY_ALLOWED_START_TIME = TunableTimeOfDay(
        description=
        '\n        The time of the day (24hr) when NPC ghosts can start doing walkbys.\n        ',
        default_hour=21)
    WALKBY_ALLOWED_DURATION = TunableRange(
        description=
        "\n        The amount of time, in sim hours, past the 'Walkby Start Time' that the\n        ghost walkbys can start.\n        ",
        tunable_type=float,
        default=5,
        minimum=0,
        maximum=23)

    @classproperty
    def source_type(cls):
        return AmbientSourceType.SOURCE_GHOST

    def is_valid(self):
        return True

    @classmethod
    def _is_correct_time(cls):
        current_time = services.time_service().sim_now
        start_time = cls.WALKBY_ALLOWED_START_TIME
        end_time = start_time + clock.interval_in_sim_hours(
            cls.WALKBY_ALLOWED_DURATION)
        return current_time.time_between_day_times(start_time, end_time)

    def get_desired_number_of_sims(self):
        if not self._is_correct_time():
            return 0
        urnstones = sims.ghost.Ghost.get_valid_urnstones()
        if not urnstones:
            return 0
        return self.DESIRED_GHOST_COUNT_PER_URNSTONE.get(len(urnstones))

    def start_appropriate_situation(self, time_of_day=None):
        urnstones = sims.ghost.Ghost.get_valid_urnstones()
        sim_info = random.choice(urnstones).get_stored_sim_info()
        resolver = SingleSimResolver(sim_info)
        for situation_type in self.GHOST_SITUATIONS(resolver=resolver):
            if self._start_specific_situation(situation_type,
                                              sim_info=sim_info):
                return True
        return False

    def _create_standard_ambient_guest_list(self, situation_type, *, sim_info):
        guest_list = SituationGuestList(invite_only=True)
        guest_list.add_guest_info(
            SituationGuestInfo(sim_info.sim_id, situation_type.default_job(),
                               RequestSpawningOption.MUST_SPAWN,
                               BouncerRequestPriority.BACKGROUND_LOW))
        return guest_list

    def get_gsi_description(self):
        return '(Ghost, {0}, {1})'.format(self.get_desired_number_of_sims(),
                                          self.get_current_number_of_sims())
Esempio n. 16
0
class Business(HasTunableReference,
               metaclass=HashedTunedInstanceMetaclass,
               manager=services.get_instance_manager(
                   sims4.resources.Types.BUSINESS)):
    INSTANCE_TUNABLES = {
        'employee_data_map':
        TunableMapping(
            description=
            '\n            The mapping between Business Employee Type and the Employee Data for\n            that type.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The Business Employee Type that should get the specified Career.\n                ',
                tunable_type=BusinessEmployeeType,
                default=BusinessEmployeeType.INVALID,
                invalid_enums=(BusinessEmployeeType.INVALID, )),
            value_type=TunableBusinessEmployeeDataSnippet(
                description=
                '\n                The Employee Data for the given Business Employee Type.\n                '
            ),
            tuning_group=GroupNames.EMPLOYEES),
        'npc_starting_funds':
        TunableRange(
            description=
            '\n            The amount of money an npc-owned store will start with in their\n            funds. Typically should be set to the same cost as the interaction\n            to buy the business.\n            ',
            tunable_type=int,
            default=0,
            minimum=0,
            tuning_group=GroupNames.CURRENCY),
        'funds_category_data':
        TunableMapping(
            description=
            '\n            Data associated with specific business funds categories.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The funds category.\n                ',
                tunable_type=BusinessFundsCategory,
                default=BusinessFundsCategory.NONE,
                invalid_enums=(BusinessFundsCategory.NONE, )),
            value_type=TunableTuple(
                description=
                '\n                The data associated with this retail funds category.\n                ',
                summary_dialog_entry=OptionalTunable(
                    description=
                    "\n                    If enabled, an entry for this category is displayed in the\n                    business' summary dialog.\n                    ",
                    tunable=TunableLocalizedString(
                        description=
                        '\n                        The dialog entry for this retail funds category. This\n                        string takes no tokens.\n                        '
                    ))),
            tuning_group=GroupNames.CURRENCY),
        'default_markup_multiplier':
        TunableRange(
            description=
            '\n            The default markup multiplier for a new business. This must match a\n            multiplier that\'s in the Markup Multiplier Data tunable. It\'s also\n            possible for this to be less than 1, meaning the default "markup"\n            will actually cause prices to be lower than normal.\n            ',
            tunable_type=float,
            default=1.25,
            minimum=math.EPSILON,
            tuning_group=GroupNames.CURRENCY),
        'advertising_name_map':
        TunableMapping(
            description=
            '\n            The mapping between advertising enum and the name used in the UI for\n            that type.\n            ',
            key_name='advertising_enum',
            key_type=TunableEnumEntry(
                description=
                '\n                The Advertising Type.\n                ',
                tunable_type=BusinessAdvertisingType,
                default=BusinessAdvertisingType.INVALID,
                invalid_enums=(BusinessAdvertisingType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            value_name='advertising_name',
            value_type=TunableLocalizedString(
                description=
                '\n                The name of the advertising type used in the UI.\n                '
            ),
            tuple_name='AdvertisingEnumDataMappingTuple',
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'advertising_type_sort_order':
        TunableList(
            description=
            '\n            Sort order for the advertising types in the UI\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The Advertising Type.\n                ',
                tunable_type=BusinessAdvertisingType,
                default=BusinessAdvertisingType.INVALID,
                invalid_enums=(BusinessAdvertisingType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            unique_entries=True,
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'quality_settings':
        TunableList(
            description=
            '\n            Tunable Business quality settings.  \n            \n            Quality type can be interpreted in different ways \n            by specific businesses, and can be used for tests.\n            \n            These are quality settings that are exported to the client.\n            \n            The order in this list should be the order we want them displayed\n            in the UI.\n            ',
            tunable=TunableTuple(
                quality_type=TunableEnumEntry(
                    description=
                    '\n                    The quality Type.\n                    ',
                    tunable_type=BusinessQualityType,
                    default=BusinessQualityType.INVALID,
                    invalid_enums=(BusinessQualityType.INVALID, ),
                    binary_type=EnumBinaryExportType.EnumUint32),
                quality_name=TunableLocalizedString(
                    description=
                    '\n                    The name of the quality type used in the UI.\n                    '
                ),
                export_class_name='QualitySettingsData'),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'show_settings_button':
        OptionalTunable(
            description=
            "\n            If enabled, this business type will show the settings button with \n            the tuned tooltip text. If disabled, this business type won't show\n            the settings button.\n            ",
            tunable=TunableLocalizedString(
                description=
                '\n                The tooltip to show on the settings button when it is shown\n                for this business type.\n                '
            ),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'business_summary_tooltip':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a business summary tooltip. If disabled, no\n            tooltip will be used or displayed by the UI.\n            ',
            tunable=TunableLocalizedString(
                description=
                '\n                The tooltip to show on the business panel.\n                '
            ),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'show_sell_button':
        Tunable(
            description=
            "\n            If checked, the sell button will be shown in the business panel if\n            the business is on the active lot. If left unchecked, the sell button\n            won't be shown on the business panel at all.\n            ",
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'show_employee_button':
        Tunable(description='\n            ',
                tunable_type=bool,
                default=False,
                tuning_group=GroupNames.UI,
                export_modes=ExportModes.ClientBinary),
        'default_quality':
        OptionalTunable(
            description=
            '\n            The default quality type for the business.',
            tunable=TunableEnumEntry(
                tunable_type=BusinessQualityType,
                default=BusinessQualityType.INVALID,
                invalid_enums=(BusinessQualityType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            disabled_value=BusinessQualityType.INVALID,
            tuning_group=GroupNames.BUSINESS),
        'quality_unlock_perk':
        OptionalTunable(
            description=
            '\n            Reference to a perk that, if unlocked, allow the player to adjust\n            the quality type specific to this business.\n            ',
            tunable=TunablePackSafeReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.BUCKS_PERK)),
            tuning_group=GroupNames.BUSINESS),
        'advertising_configuration':
        AdvertisingConfiguration.TunableFactory(
            description=
            '\n            Tunable Business advertising configuration.\n            ',
            tuning_group=GroupNames.BUSINESS),
        'markup_multiplier_data':
        TunableList(
            description=
            '\n            A list of markup multiplier display names and the actual multiplier\n            associated with that name. This is used for sending the markup\n            information to the UI.\n            ',
            tunable=TunableTuple(
                description=
                '\n               A tuple of the markup multiplier display name and the actual\n               multiplier associated with that display name.\n               ',
                name=TunableLocalizedString(
                    description=
                    '\n                   The display name for this markup multiplier. e.g. a\n                   multiplier of 1.2 will have "20 %" tuned here.\n                   '
                ),
                markup_multiplier=TunableRange(
                    description=
                    '\n                    The multiplier associated with this display name.\n                    ',
                    tunable_type=float,
                    default=1,
                    minimum=math.EPSILON),
                export_class_name='MarkupMultiplierData'),
            tuning_group=GroupNames.CURRENCY,
            export_modes=ExportModes.All),
        'star_rating_to_screen_slam_map':
        TunableMapping(
            description=
            '\n            A mapping of star ratings to screen slams.\n            Screen slams will be triggered when the rating increases to a new\n            whole value.\n            ',
            key_type=int,
            value_type=ui.screen_slam.TunableScreenSlamSnippet(),
            key_name='star_rating',
            value_name='screen_slam',
            tuning_group=GroupNames.BUSINESS),
        'show_empolyee_skill_level_up_notification':
        Tunable(
            description=
            '\n            If true, skill level up notifications will be shown for employees.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.EMPLOYEES),
        'bucks':
        TunableEnumEntry(
            description=
            '\n            The Bucks Type this business will use for Perk unlocks.\n            ',
            tunable_type=BucksType,
            default=BucksType.INVALID,
            invalid_enums=(BucksType.INVALID, ),
            tuning_group=GroupNames.CURRENCY,
            export_modes=ExportModes.All),
        'off_lot_star_rating_decay_multiplier_perk':
        OptionalTunable(
            description=
            '\n            If enabled, allows the tuning of a perk which can adjust the off-lot star rating decay.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The off lot star rating decay multiplier tuning.\n                ',
                perk=TunableReference(
                    description=
                    '\n                    The perk that will cause the multiplier to be applied to the\n                    star rating decay during off-lot simulations.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.BUCKS_PERK)),
                decay_multiplier=TunableRange(
                    description=
                    '\n                    If the household has the specified perk, the off-lot star\n                    rating decay rate will be multiplied by this value.\n                    ',
                    tunable_type=float,
                    default=1.1,
                    minimum=0)),
            tuning_group=GroupNames.OFF_LOT),
        'manage_outfit_affordances':
        TunableSet(
            description=
            '\n            A list of affordances that are shown when the player clicks on the\n            Manage Outfits button.\n            ',
            tunable=TunableReference(
                description=
                '\n                An affordance shown when the player clicks on the Manage Outfits\n                button.\n                ',
                manager=services.affordance_manager(),
                pack_safe=True),
            tuning_group=GroupNames.EMPLOYEES),
        'employee_training_buff_tag':
        TunableEnumWithFilter(
            description=
            '\n            A tag to indicate a buff is used for employee training.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID,
            invalid_enums=(tag.Tag.INVALID, ),
            filter_prefixes=('buff', ),
            pack_safe=True,
            tuning_group=GroupNames.EMPLOYEES),
        'customer_buffs_to_save_tag':
        TunableEnumWithFilter(
            description=
            '\n            All buffs with this tag will be saved and reapplied to customer sims\n            on load.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID,
            invalid_enums=(tag.Tag.INVALID, ),
            filter_prefixes=('buff', ),
            pack_safe=True,
            tuning_group=GroupNames.CUSTOMER),
        'customer_buffs_to_remove_tags':
        TunableSet(
            description=
            '\n            Tags that indicate which buffs should be removed from customers when\n            they leave the business.\n            ',
            tunable=TunableEnumWithFilter(
                description=
                '\n                A tag that indicates a buff should be removed from the customer\n                when they leave the business.\n                ',
                tunable_type=tag.Tag,
                default=tag.Tag.INVALID,
                invalid_enums=(tag.Tag.INVALID, ),
                filter_prefixes=('buff', ),
                pack_safe=True),
            tuning_group=GroupNames.CUSTOMER),
        'current_business_lot_transfer_dialog_entry':
        TunableLocalizedString(
            description=
            '\n            This is the text that will show in the funds transfer dialog drop\n            down for the current lot if it\'s a business lot. Typically, the lot\n            name would show but if the active lot is a business lot it makes\n            more sense to say something along the lines of\n            "Current Retail Lot" or "Current Restaurant" instead of the name of the lot.\n            ',
            tuning_group=GroupNames.UI),
        'open_business_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that shows up when the player opens the business.\n            We need to trigger this from code because we need the notification\n            to show up when we open the store through the UI or through an\n            Interaction.\n            ',
            tuning_group=GroupNames.UI),
        'no_way_to_make_money_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that shows up when the player opens a store that has no\n            way of currently making money (e.g. retail store having no items set for\n            sale or restaurants having nothing on the menu). It will replace the\n            Open Business Notification.\n            ',
            tuning_group=GroupNames.UI),
        'audio_sting_open':
        TunablePlayAudioAllPacks(
            description=
            '\n            The audio sting to play when the store opens.\n            ',
            tuning_group=GroupNames.UI),
        'audio_sting_close':
        TunablePlayAudioAllPacks(
            description=
            '\n            The audio sting to play when the store closes.\n            ',
            tuning_group=GroupNames.UI),
        'sell_store_dialog':
        UiDialogOkCancel.TunableFactory(
            description=
            '\n            This dialog is to confirm the sale of the business.\n            ',
            tuning_group=GroupNames.UI),
        'lighting_helper_open':
        LightingHelper.TunableFactory(
            description=
            '\n            The lighting helper to execute when the store opens.\n            e.g. Turn on all neon signs.\n            ',
            tuning_group=GroupNames.TRIGGERS),
        'lighting_helper_close':
        LightingHelper.TunableFactory(
            description=
            '\n            The lighting helper to execute when the store closes.\n            e.g. Turn off all neon signs.\n            ',
            tuning_group=GroupNames.TRIGGERS),
        'min_and_max_star_rating':
        TunableInterval(
            description=
            '\n            The lower and upper bounds for a star rating. This affects both the\n            customer star rating and the overall business star rating.\n            ',
            tunable_type=float,
            default_lower=1,
            default_upper=5,
            tuning_group=GroupNames.BUSINESS),
        'min_and_max_star_rating_value':
        TunableInterval(
            description=
            '\n            The minimum and maximum star rating value for this business.\n            ',
            tunable_type=float,
            default_lower=0,
            default_upper=100,
            tuning_group=GroupNames.BUSINESS),
        'star_rating_value_to_user_facing_rating_curve':
        TunableCurve(
            description=
            '\n           Curve that maps star rating values to the user-facing star rating.\n           ',
            x_axis_name='Star Rating Value',
            y_axis_name='User-Facing Star Rating',
            tuning_group=GroupNames.BUSINESS),
        'default_business_star_rating_value':
        TunableRange(
            description=
            '\n            The star rating value a newly opened business will begin with. Keep in mind, this is not the actual star rating. This is the value which maps to a rating using \n            ',
            tunable_type=float,
            default=1,
            minimum=0,
            tuning_group=GroupNames.BUSINESS),
        'customer_rating_delta_to_business_star_rating_value_change_curve':
        TunableCurve(
            description=
            '\n            When a customer is done with their meal, we will take the delta\n            between their rating and the business rating and map that to an\n            amount it should change the star rating value for the restaurant.\n            \n            For instance, the business has a current rating of 3 stars and the\n            customer is giving a rating of 4.5 stars. 4.5 - 3 = a delta of 1.5.\n            That 1.5 will map, on this curve, to the amount we should adjust the\n            star rating value for the business.\n            ',
            x_axis_name=
            'Customer Rating to Business Rating Delta (restaurant rating - customer rating)',
            y_axis_name='Business Star Rating Value Change',
            tuning_group=GroupNames.BUSINESS),
        'default_customer_star_rating':
        TunableRange(
            description=
            '\n            The star rating a new customer starts with.\n            ',
            tunable_type=float,
            default=3,
            minimum=0,
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_buff_bucket_data':
        TunableMapping(
            description=
            "\n            A mapping from Business Customer Star Rating Buff Bucket to the data\n            associated with the buff bucker for this business.\n            \n            Each buff bucket has a minimum, median, and maximum value. For every\n            buff a customer has that falls within a buff bucket, that buff's\n            Buff Bucket Delta is added to that bucket's totals. The totals are\n            clamped between -1 and 1 and interpolated against the\n            minimum/medium/maximum value for their associated buckets. All of\n            the final values of the buckets are added together and that value is\n            used in the Customer Star Buff Bucket To Rating Curve to determine\n            the customer's final star rating of this business.\n            \n            For instance, assume a buff bucket has a minimum value of -200, median of 0,\n            and maximum of 100, and the buff bucket's clamped total is 0.5, the actual\n            value of that bucket will be 50 (half way, or 0.5, between 0 and\n            100). If, however, the bucket's total is -0.5, we'd interpolate\n            between the bucket's minimum value, -200, and median value, 0, to arrive at a\n            bucket value of -100.\n            ",
            key_name='Star_Rating_Buff_Bucket',
            key_type=TunableEnumEntry(
                description=
                '\n                The Business Customer Star Rating Buff Bucket enum.\n                ',
                tunable_type=BusinessCustomerStarRatingBuffBuckets,
                default=BusinessCustomerStarRatingBuffBuckets.INVALID,
                invalid_enums=(
                    BusinessCustomerStarRatingBuffBuckets.INVALID, )),
            value_name='Star_Rating_Buff_Bucket_Data',
            value_type=TunableTuple(
                description=
                '\n                All of the data associated with a specific customer star rating\n                buff bucket.\n                ',
                bucket_value_minimum=Tunable(
                    description=
                    "\n                    The minimum value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=-100),
                positive_bucket_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    The vfx to play when positive change star value occurs. \n                    '
                ),
                negative_bucket_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    The vfx to play when negative change star value occurs.\n                    '
                ),
                bucket_value_median=Tunable(
                    description=
                    "\n                    The median/middle value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=0),
                bucket_value_maximum=Tunable(
                    description=
                    "\n                    The maximum value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=100),
                bucket_icon=TunableIconAllPacks(
                    description=
                    '\n                    The icon that represents this buff bucket.\n                    '
                ),
                bucket_positive_text=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The possible text strings to show up when this bucket\n                    results in a positive star rating.\n                    '
                ),
                bucket_negative_text=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The possible text strings to show up when this bucket\n                    results in a bad star rating.\n                    '
                ),
                bucket_excellence_text=TunableLocalizedStringFactoryVariant(
                    description=
                    "\n                    The description text to use in the business summary panel if\n                    this buff bucket is in the 'Excellence' section.\n                    "
                ),
                bucket_growth_opportunity_text=
                TunableLocalizedStringFactoryVariant(
                    description=
                    "\n                    The description text to use in the business summary panel if\n                    this buff bucket is in the 'Growth Opportunity' section.\n                    "
                ),
                bucket_growth_opportunity_threshold=TunableRange(
                    description=
                    '\n                    The amount of score this bucket must be from the maximum to be\n                    considered a growth opportunity. \n                    ',
                    tunable_type=float,
                    minimum=0,
                    default=10),
                bucket_excellence_threshold=TunableRange(
                    description=
                    '\n                    The amount of score this bucket must be before it is \n                    considered an excellent bucket\n                    ',
                    tunable_type=float,
                    minimum=0,
                    default=1),
                bucket_title=TunableLocalizedString(
                    description=
                    '\n                    The name for this bucket.\n                    '
                )),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_buff_data':
        TunableMapping(
            description=
            '\n            A mapping of Buff to the buff data associated with that buff.\n            \n            Refer to the description on Customer Star Rating Buff Bucket Data\n            for a detailed explanation of how this tuning works.\n            ',
            key_name='Buff',
            key_type=Buff.TunableReference(
                description=
                "\n                A buff meant to drive a customer's star rating for a business.\n                ",
                pack_safe=True),
            value_name='Buff Data',
            value_type=TunableTuple(
                description=
                '\n                The customer star rating for this buff.\n                ',
                buff_bucket=TunableEnumEntry(
                    description=
                    '\n                    The customer star rating buff bucket associated with this buff.\n                    ',
                    tunable_type=BusinessCustomerStarRatingBuffBuckets,
                    default=BusinessCustomerStarRatingBuffBuckets.INVALID,
                    invalid_enums=(
                        BusinessCustomerStarRatingBuffBuckets.INVALID, )),
                buff_bucket_delta=Tunable(
                    description=
                    '\n                    The amount of change this buff should contribute to its bucket.\n                    ',
                    tunable_type=float,
                    default=0),
                update_star_rating_on_add=Tunable(
                    description=
                    "\n                    If enabled, the customer's star rating will be re-\n                    calculated when this buff is added.\n                    ",
                    tunable_type=bool,
                    default=True),
                update_star_rating_on_remove=Tunable(
                    description=
                    "\n                    If enabled, the customer's star rating will be re-\n                    calculated when this buff is removed.\n                    ",
                    tunable_type=bool,
                    default=False)),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_buff_bucket_to_rating_curve':
        TunableCurve(
            description=
            '\n            A mapping of the sum of all buff buckets for a single customer to\n            the star rating for that customer.\n            \n            Refer to the description on Customer Star Rating Buff Bucket Data\n            for a detailed explanation of how this tuning works.\n            ',
            x_axis_name='Buff Bucket Total',
            y_axis_name='Star Rating',
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_increase_arrow':
        OptionalTunable(
            description=
            '\n            The "up arrow" VFX to play when a customer\'s star rating goes up.\n            These will play even if the customer\'s rating doesn\'t go up enough\n            to trigger a star change.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_decrease_arrow':
        OptionalTunable(
            description=
            '\n            The "down arrow" VFX to play when a customer\'s star rating goes\n            down. These will play even if the customer\'s rating doesn\'t go down\n            enough to trigger a star change.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_mapping':
        TunableStarRatingVfxMapping(
            description=
            '\n            Maps the star rating for the customer to the persistent star effect\n            that shows over their head.\n            ',
            tuning_group=GroupNames.CUSTOMER),
        'customer_final_star_rating_vfx':
        OptionalTunable(
            description=
            '\n            The VFX to play when the customer is done and is submitting their\n            final star rating to the business.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_max_star_rating_vfx':
        OptionalTunable(
            description=
            '\n            The VFX to play when the customer hits the maximum star rating.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_statistic':
        TunablePackSafeReference(
            description=
            '\n            The statistic on a customer Sim that represents their current star\n            rating.\n            ',
            manager=services.get_instance_manager(Types.STATISTIC),
            allow_none=True,
            tuning_group=GroupNames.CUSTOMER),
        'buy_business_lot_affordance':
        TunableReference(
            description=
            '\n            The affordance to buy a lot for this type of business.\n            ',
            manager=services.get_instance_manager(Types.INTERACTION),
            tuning_group=GroupNames.UI),
        'initial_funds_transfer_amount':
        TunableRange(
            description=
            '\n            The amount to default the funds transfer dialog when a player\n            initially buys this business.\n            ',
            tunable_type=int,
            minimum=0,
            default=2500,
            tuning_group=GroupNames.CURRENCY),
        'summary_dialog_icon':
        TunableIcon(
            description=
            '\n            The Icon to show in the header of the dialog.\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_subtitle':
        TunableLocalizedString(
            description=
            "\n            The subtitle for the dialog. The main title will be the store's name.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_transactions_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Items Sold' line item. By design, this should say\n            something along the lines of 'Items Sold:' or 'Transactions:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_transactions_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Items Sold' line item. By design, this should say\n            the number of items sold.\n            {0.Number} = number of items sold since the store was open\n            i.e. {0.Number}\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_cost_of_ingredients_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Cost of Ingredients' line item. By design, this\n            should say something along the lines of 'Cost of Ingredients:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_cost_of_ingredients_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Cost of Ingredients' line item. {0.Number} = the\n            amount of money spent on ingredients.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_food_profit_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Food Profits' line item. This line item is the\n            total revenue minus the cost of ingredients. By design, this should\n            say something along the lines of 'Food Profits:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_food_profit_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Food Profits' line item. {0.Number} = the amount of\n            money made on food.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_owed_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Wages Owned' line item. By design, this\n            should say 'Wages Owed:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_owed_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Wages Owed' line item. By design, this should say the\n            number of hours worked and the price per hour.\n            {0.Number} = number of hours worked by all employees\n            {1.Money} = amount employees get paid per hour\n            i.e. {0.Number} hours worked x {1.Money}/hr\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_payroll_header':
        TunableLocalizedStringFactory(
            description=
            '\n            The header text for each unique Sim on payroll. This is provided one\n            token, the Sim.\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_payroll_text':
        TunableLocalizedStringFactory(
            description=
            '\n            The text for each job that the Sim on payroll has held today. This is\n            provided three tokens: the career level name, the career level salary,\n            and the total hours worked.\n            \n            e.g.\n             {0.String} ({1.Money}/hr) * {2.Number} {S2.hour}{P2.hours}\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_advertising_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Advertising' line item. By design, this\n            should say 'Advertising Spent:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_advertising_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Advertising' line item. By design, this should say the\n            amount spent on advertising\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_net_profit_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Net Profit' line item. By design, this\n            should say 'Net Profit:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_net_profit_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Net Profit' line item. By design, this should say the\n            total amount earnt so far in this shift\n            ",
            tuning_group=GroupNames.UI),
        'grand_opening_notification':
        OptionalTunable(
            description=
            '\n            If enabled, allows a notification to be tuned that will show only\n            the first time you arrive on your business lot.\n            ',
            tunable=TunableUiDialogNotificationSnippet(),
            tuning_group=GroupNames.UI),
        'business_icon':
        TunableIcon(
            description=
            '\n            The Icon to show in the header of the dialog.\n            ',
            tuning_group=GroupNames.UI),
        'star_rating_to_customer_count_curve':
        TunableCurve(
            description=
            '\n            A curve mapping of current star rating of the restaurant to the base\n            number of customers that should come per interval.\n            ',
            x_axis_name='Star Rating',
            y_axis_name='Base Customer Count',
            tuning_group=GroupNames.CUSTOMER),
        'time_of_day_to_customer_count_multiplier_curve':
        TunableCurve(
            description=
            '\n            A curve that lets you tune a specific customer multiplier based on the \n            time of day. \n            \n            Time of day should range between 0 and 23, 0 being midnight.\n            ',
            tuning_group=GroupNames.CUSTOMER,
            x_axis_name='time_of_day',
            y_axis_name='customer_multiplier'),
        'off_lot_customer_count_multiplier':
        TunableRange(
            description=
            '\n            This value will be multiplied by the Base Customer Count (derived\n            from the Star Rating To Customer Count Curve) to determine the base\n            number of customers per hour during off-lot simulation.\n            ',
            tunable_type=float,
            minimum=0,
            default=0.5,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_customer_count_penalty_multiplier':
        TunableRange(
            description=
            '\n            A penalty multiplier applied to the off-lot customer count. This is\n            applied after the Off Lot Customer Count Multiplier is applied.\n            ',
            tunable_type=float,
            default=0.2,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_chance_of_star_rating_increase':
        TunableRange(
            description=
            "\n            Every time we run offlot simulations, we'll use this as the chance\n            to increase in star rating instead of decrease.\n            ",
            tunable_type=float,
            default=0.1,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_star_rating_decay_per_hour_curve':
        TunableCurve(
            description=
            '\n            Maps the current star rating of the business to the decay per hour\n            of star rating value. This value will be added to the current star\n            rating value so use negative numbers to make the rating decay.\n            ',
            x_axis_name='Business Star Rating',
            y_axis_name='Off-Lot Star Rating Value Decay Per Hour',
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_star_rating_increase_per_hour_curve':
        TunableCurve(
            description=
            '\n            Maps the current star rating of the business to the increase per\n            hour of the star rating value, assuming the Off Lot Chance Of Star\n            Rating Increase passes.\n            ',
            x_axis_name='Business Star Rating',
            y_axis_name='Off-Lot Star Rating Value Increase Per Hour',
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_profit_per_item_multiplier':
        TunableRange(
            description=
            '\n            This is multiplied by the average cost of the business specific\n            service that is the main source of profit, to determine how much \n            money the business makes per customer during off-lot simulation.\n            ',
            tunable_type=float,
            default=0.3,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_net_loss_notification':
        OptionalTunable(
            description=
            '\n            If enabled, the notification that will show if a business turns a \n            negative net profit during off-lot simulation.\n            ',
            tunable=TunableUiDialogNotificationSnippet(),
            tuning_group=GroupNames.OFF_LOT),
        'critic':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a critic for this business type.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Critic tuning for this business.\n                ',
                critic_trait=TunableReference(
                    description=
                    '\n                    The trait used to identify a critic of this business.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.TRAIT)),
                critic_star_rating_application_count=TunableRange(
                    description=
                    '\n                    The number of times a critics star rating should count towards the\n                    business star rating.\n                    ',
                    tunable_type=int,
                    default=10,
                    minimum=1),
                critic_star_rating_vfx_mapping=TunableStarRatingVfxMapping(
                    description=
                    '\n                    Maps the star rating for the critic to the persistent star effect\n                    that shows over their head.\n                    '
                ),
                critic_banner_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    A persistent banner VFX that is started when the critic\n                    arrives and stopped when they leave.\n                    '
                )),
            tuning_group=GroupNames.CUSTOMER)
    }

    @classmethod
    def _verify_tuning_callback(cls):
        advertising_data_types = frozenset(
            cls.advertising_configuration.advertising_data_map.keys())
        advertising_types_with_mapped_names = frozenset(
            cls.advertising_name_map.keys())
        advertising_sort_ordered_types = frozenset(
            cls.advertising_name_map.keys())
        if advertising_data_types:
            if advertising_data_types != advertising_types_with_mapped_names:
                logger.error(
                    'Advertising type list {} does not match list of mapped names: {}',
                    advertising_data_types,
                    advertising_types_with_mapped_names)
            if advertising_data_types != advertising_sort_ordered_types:
                logger.error(
                    'Advertising type list {} does not sorted UI list types: {}',
                    advertising_data_types, advertising_sort_ordered_types)
        if cls.advertising_configuration.default_advertising_type is not None and cls.advertising_configuration.default_advertising_type not in advertising_types_with_mapped_names:
            logger.error(
                'Default advertising type {} is not in advertising name map',
                cls.default_advertising_type)
Esempio n. 17
0
class RetailZoneDirector(BusinessZoneDirectorMixin, SchedulingZoneDirector):
    INSTANCE_TUNABLES = {'customer_count_curb_appeal_curve': TunableCurve(description='\n            The number of customers we want on the lot based on the curb appeal of\n            the lot. This only determines how many customers we want on the lot.\n            The type of customer is driven by the Customer Data Map and the average\n            value of sellable items on the lot.\n            ', x_axis_name='Curb Appeal', y_axis_name='Customer Count', tuning_group=GroupNames.BUSINESS), 'customer_situations': TunableMapping(description='\n            A mapping that defines which customer situations are spawned based\n            on certain properties of the retail lot.\n            ', key_name='Markup Multiplier', key_type=Tunable(description="\n                The store's price multiplier.\n                ", tunable_type=float, default=1), value_type=TunableList(description='\n                A list of tuple defining the customer data for this multiplier.\n                ', tunable=TunableTuple(required_median_value=TunableInterval(description='\n                        The median value of all items in the store must fall within\n                        this interval, which is inclusive.\n                        ', tunable_type=float, default_lower=0, default_upper=MAX_FLOAT, minimum=0), weighted_situations=TunableList(description='\n                        A list of situations that are available in the specified\n                        markup and price range combination. The situations are\n                        weighted relative to one another within this list.\n                        ', tunable=TunableTuple(situation=RetailCustomerSituation.TunableReference(description="\n                                The situation defining the customer's behavior.\n                                ", pack_safe=True), weight=Tunable(description="\n                                This situation's weight, relative to other\n                                situations in this list.\n                                ", tunable_type=float, default=1))))), tuning_group=GroupNames.BUSINESS), 'employee_situations': TunableList(description='\n            The list of possible employee situations. Right now, one will be\n            assigned at random when the employee comes to work.\n            ', tunable=RetailEmployeeSituation.TunableReference(), tuning_group=GroupNames.BUSINESS), 'npc_employee_situation': RetailEmployeeSituation.TunableReference(description='\n            The situation NPC employees will run.\n            ', tuning_group=GroupNames.BUSINESS)}
    CUSTOMER_SITUATION_LIST_GUID = 258695776
    EMPLOYEE_SITUATION_LIST_GUID = 2967593715

    def _should_create_npc_business_manager(self):
        return True

    def _get_new_npc_business_manager(self):
        npc_business_manager = RetailManager()
        npc_business_manager.set_zone_id(services.current_zone_id())
        npc_business_manager.set_owner_household_id(None)
        return npc_business_manager

    def _load_custom_zone_director(self, zone_director_proto, reader):
        for situation_data_proto in zone_director_proto.situations:
            if situation_data_proto.situation_list_guid == self.CUSTOMER_SITUATION_LIST_GUID:
                self._customer_situation_ids.extend(situation_data_proto.situation_ids)
            elif situation_data_proto.situation_list_guid == self.EMPLOYEE_SITUATION_LIST_GUID:
                self._employee_situation_id_list.extend(situation_data_proto.situation_ids)
        super()._load_custom_zone_director(zone_director_proto, reader)

    def _load_employee_situations(self, zone_director_proto, reader):
        pass

    def _save_custom_zone_director(self, zone_director_proto, writer):
        situation_data_proto = zone_director_proto.situations.add()
        situation_data_proto.situation_list_guid = self.CUSTOMER_SITUATION_LIST_GUID
        situation_data_proto.situation_ids.extend(self._customer_situation_ids)
        if self.business_manager is not None:
            if not self.business_manager.is_owned_by_npc:
                situation_data_proto = zone_director_proto.situations.add()
                situation_data_proto.situation_list_guid = self.EMPLOYEE_SITUATION_LIST_GUID
                for situation_ids in self._employee_situation_ids.values():
                    situation_data_proto.situation_ids.extend(situation_ids)
        super()._save_custom_zone_director(zone_director_proto, writer)

    def _save_employee_situations(self, zone_director_proto, writer):
        pass

    def _get_employee_situation_for_employee_type(self, employee_type):
        return random.choice(self.employee_situations)

    def _get_npc_employee_situation_for_employee_type(self, employee_type):
        return self.npc_employee_situation

    def create_situations_during_zone_spin_up(self):
        is_owned_business = self.business_manager is not None and self.business_manager.owner_household_id is not None
        if is_owned_business and (not self.business_manager.is_owner_household_active and (services.current_zone().time_has_passed_in_world_since_zone_save() or services.current_zone().active_household_changed_between_save_and_load())) and self.business_manager.is_open:
            self._business_manager.start_already_opened_business()
        if is_owned_business:
            return
        super().create_situations_during_zone_spin_up()

    def _get_valid_customer_situations(self, business_manager):
        median_item_value = business_manager.get_median_item_value()
        markup_multiplier = business_manager.markup_multiplier
        for (customer_situation_markup_multiplier, customer_situation_datas) in self.customer_situations.items():
            if almost_equal(markup_multiplier, customer_situation_markup_multiplier):
                break
        else:
            return ()
        valid_situations = []
        resolver = SingleSimResolver(services.active_sim_info())
        for customer_situation_data in customer_situation_datas:
            if median_item_value in customer_situation_data.required_median_value:
                valid_situations.extend((pair.weight, pair.situation) for pair in customer_situation_data.weighted_situations if pair.situation.can_start_situation(resolver))
        return valid_situations

    def _on_customer_situation_request(self):
        self.remove_stale_customer_situations()
        desired_situation_count = self.customer_count_curb_appeal_curve.get(self._business_manager.get_curb_appeal())
        valid_weighted_situations = self._get_valid_customer_situations(self._business_manager)
        if not valid_weighted_situations:
            logger.warn('Tried finding a valid starting situation for customer but no situations matches were found.')
            return
        while desired_situation_count > len(self._customer_situation_ids):
            situation_to_start = weighted_random_item(valid_weighted_situations)
            if situation_to_start is None:
                break
            self.start_customer_situation(situation_to_start)

    @property
    def supported_business_types(self):
        return SUPPORTED_BUSINESS_TYPES
Esempio n. 18
0
class RestaurantTuning:
    MENU_PRESETS = TunableMapping(
        description=
        '\n        The map to tune preset of menus that player to select to use in\n        restaurant customization.\n        ',
        key_type=TunableEnumEntry(tunable_type=MenuPresets,
                                  default=MenuPresets.CUSTOMIZE,
                                  binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description='\n            Menu preset contents.\n            ',
            preset_name=TunableLocalizedString(
                description=
                '\n                Menu preset name that appear in both menu customize UI and in\n                game menu UI.\n                '
            ),
            recipe_map=TunableMapping(
                description=
                "\n                The map that represent a menu preset. It's organized with courses\n                like drink, appetizer, entree etc, and in each course there are\n                options of recipes.\n                ",
                key_type=TunableEnumWithFilter(
                    tunable_type=Tag,
                    filter_prefixes=['recipe_course'],
                    default=Tag.INVALID,
                    invalid_enums=(Tag.INVALID, ),
                    pack_safe=True,
                    binary_type=EnumBinaryExportType.EnumUint32),
                value_type=TunableSet(
                    tunable=TunableReference(manager=services.recipe_manager(),
                                             class_restrictions=('Recipe', ),
                                             pack_safe=True)),
                key_name='course_tags',
                value_name='recipes',
                tuple_name='MenuCourseMappingTuple'),
            show_in_restaurant_menu=Tunable(
                description=
                "\n                If this is enabled, this menu preset will show up on restaurant\n                menus. If not, it won't. Currently, only home-chef menus\n                shouldn't show up on restaurant menus.\n                ",
                tunable_type=bool,
                default=True),
            export_class_name='MenuPresetContentTuple'),
        key_name='preset_enum',
        value_name='preset_contents',
        tuple_name='MenuPresetMappingTuple',
        export_modes=ExportModes.All)
    MENU_TAG_DISPLAY_CONTENTS = TunableMapping(
        description=
        '\n        The map to tune menu tags to display contents.\n        ',
        key_type=TunableEnumWithFilter(
            tunable_type=Tag,
            filter_prefixes=['recipe'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description=
            '\n            menu tag display contents.\n            ',
            menu_tag_name=TunableLocalizedString(),
            menu_tag_icon=TunableResourceKey(
                description=
                '\n                This will display as the filter icon in the course recipe picker UI.\n                ',
                resource_types=sims4.resources.CompoundTypes.IMAGE),
            export_class_name='MenuTagDisplayTuple'),
        key_name='menu_tags',
        value_name='menu_tag_display_contents',
        tuple_name='MenuTagDisplayMappingTuple',
        export_modes=ExportModes.ClientBinary)
    COURSE_SORTING_SEQUENCE = TunableSet(
        description=
        '\n        This set determines the sorting sequence for courses in both menu\n        customize UI and in game menu UI.\n        ',
        tunable=TunableEnumWithFilter(
            tunable_type=Tag,
            filter_prefixes=['recipe_course'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        export_modes=ExportModes.ClientBinary)
    DAILY_SPECIAL_DISCOUNT = TunablePercent(
        description=
        '\n        The percentage of the base price when an item is the daily special.\n        For example, if the base price is $10 and this is tuned to 80%, the\n        discounted price will be $10 x 80% = $8\n        ',
        default=80)
    INVALID_DAILY_SPECIAL_RECIPES = TunableList(
        description=
        '\n        A list of recipes that should not be considered for daily specials.\n        i.e. Glass of water.\n        ',
        tunable=TunableReference(
            description=
            '\n            The recipe to disallow from being a daily special.\n            ',
            manager=services.recipe_manager(),
            class_restrictions=('Recipe', ),
            pack_safe=True))
    COURSE_TO_FILTER_TAGS_MAPPING = TunableMapping(
        description=
        '\n        Mapping from course to filter tags for food picker UI.\n        ',
        key_type=TunableEnumWithFilter(
            description=
            '\n            The course associated with the list of filters.\n            ',
            tunable_type=Tag,
            filter_prefixes=['recipe_course'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableList(
            description=
            '\n            This list of filter tags for the food picker UI for the course\n            specified.\n            ',
            tunable=TunableEnumWithFilter(
                tunable_type=Tag,
                filter_prefixes=['recipe_category'],
                default=Tag.INVALID,
                invalid_enums=(Tag.INVALID, ),
                pack_safe=True,
                binary_type=EnumBinaryExportType.EnumUint32)),
        key_name='course_key',
        value_name='course_filter_tags',
        tuple_name='CourseToFilterTuple',
        export_modes=ExportModes.ClientBinary)
    CUSTOMER_QUALITY_STAT = TunablePackSafeReference(
        description=
        '\n        The Customer Quality stat applied to food/drink the restaurant customer\n        eats/drinks. This is how we apply buffs to the Sim at the time they\n        consume the food/drink.\n        \n        The Customer Quality value is determined by multiplying the Final\n        Quality To Customer Quality Multiplier (found in Final Quality State\n        Data Mapping) by the Food Difficulty To Customer Quality Multiplier\n        (found in the Ingredient Quality State Data Mapping).\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))
    CUSTOMER_VALUE_STAT = TunablePackSafeReference(
        description=
        '\n        The Customer Value stat applied to food/drink the restaurant customer\n        eats/drinks. This is how we apply buffs to the Sim at the time they\n        consume the food/drink.\n        \n        The Customer Value value is determined by multiplying the Final Quality\n        To Customer Value Multiplier (found in Final Quality State Data Mapping)\n        by the Markup To Customer Value Multiplier (found in the Markup Data\n        Mapping).\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))
    RECIPE_DIFFICULTY_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of the recipe difficulty for restaurants to the appropriate\n        data.\n        ',
        key_name='recipe_difficulty',
        key_type=TunableEnumEntry(
            description=
            "\n            The recipe difficulty for chef's at a restaurant.\n            ",
            tunable_type=RecipeDifficulty,
            default=RecipeDifficulty.NORMAL),
        value_name='recipe_difficulty_data',
        value_type=TunableTuple(
            description=
            '\n            The tuning associated with the provided recipe difficulty.\n            ',
            recipe_difficulty_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Ingredient Quality To Final Quality Adder\n                and the Cooking Speed To Final Quality Adder to determine the player-\n                facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            recipe_difficulty_to_customer_quality_multiplier=Tunable(
                description=
                "\n                This value is multiplied by the Final Quality To Customer\n                Quality Multiplier to determine the customer's perceived quality\n                of the recipe.\n                ",
                tunable_type=float,
                default=1)))
    DEFAULT_INGREDIENT_QUALITY = TunableEnumEntry(
        description=
        '\n        The default ingredient quality for a restaurant.\n        ',
        tunable_type=RestaurantIngredientQualityType,
        default=RestaurantIngredientQualityType.INVALID,
        invalid_enums=(RestaurantIngredientQualityType.INVALID, ))
    INGREDIENT_QUALITY_DATA_MAPPING = TunableMapping(
        description=
        '\n        The mapping between ingredient enum and the ingredient data for\n        that type.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The ingredient type. Organic, normal, lousy, etc...\n            ',
            tunable_type=RestaurantIngredientQualityType,
            default=RestaurantIngredientQualityType.INVALID,
            invalid_enums=(RestaurantIngredientQualityType.INVALID, ),
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description=
            '\n            Data associated with this type of ingredient.\n            ',
            ingredient_quality_type_name=TunableLocalizedString(
                description=
                '\n                The localized name of this ingredient used in various places in\n                the UI.\n                '
            ),
            ingredient_quality_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Recipe Difficulty To Final Quality\n                Adder and the Cooking Speed To Final Quality Adder to determine\n                the player-facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            ingredient_quality_to_restaurant_expense_multiplier=TunableRange(
                description=
                '\n                This value is multiplied by the Base Restaurant Price (found in\n                the Recipe tuning) for each recipe served to determine what the\n                cost is to the restaurant for preparing that recipe.\n                ',
                tunable_type=float,
                default=0.5,
                minimum=0),
            export_class_name='IngredientDataTuple'),
        key_name='ingredient_enum',
        value_name='ingredient_data',
        tuple_name='IngredientEnumDataMappingTuple',
        export_modes=ExportModes.All)
    COOKING_SPEED_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping from chef cooking speed to the data associated with that\n        cooking speed.\n        ',
        key_name='cooking_speed_buff',
        key_type=TunableReference(
            description=
            '\n            The cooking speed buff that is applied to the chef.\n            ',
            manager=services.get_instance_manager(sims4.resources.Types.BUFF),
            pack_safe=True),
        value_name='cooking_speed_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned cooking speed.\n            ',
            cooking_speed_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Recipe Difficulty To Final Quality\n                Adder and the Ingredient Quality To Final Quality Adder to\n                determine the player-facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            active_cooking_states_delta=Tunable(
                description=
                '\n                The amount by which to adjust the number of active cooking\n                states the chef must complete before completing the order. For\n                instance, if a -1 is tuned here, the chef will have to complete\n                one less state than normal. Regardless of how the buffs are\n                tuned, the chef will always run at least one state before\n                completing the order.\n                ',
                tunable_type=int,
                default=-1)))
    CHEF_SKILL_TO_FOOD_FINAL_QUALITY_ADDER_DATA = TunableTuple(
        description=
        '\n        Pairs a skill with a curve to determine the additional value to add to\n        the final quality of a food made at an owned restaurant.\n        ',
        skill=TunablePackSafeReference(
            description=
            '\n            The skill used to determine the adder for the final quality of food.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions=('Skill', )),
        final_quality_adder_curve=TunableCurve(
            description=
            "\n            Maps the chef's current level of the tuned skill to a value that\n            will be added to the final quality statistic for food recipes cooked\n            at an owned restaurant.\n            ",
            x_axis_name='Skill Level',
            y_axis_name='Food Final Quality Adder'))
    CHEF_SKILL_TO_DRINK_FINAL_QUALITY_ADDER_DATA = TunableTuple(
        description=
        '\n        Pairs a skill with a curve to determine the additional value to add to\n        the final quality of a drink made at an owned restaurant.\n        ',
        skill=TunablePackSafeReference(
            description=
            '\n            The skill used to determine the adder for the final quality of drinks.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions=('Skill', )),
        final_quality_adder_curve=TunableCurve(
            description=
            "\n            Maps the chef's current level of the tuned skill to a value that\n            will be added to the final quality statistic for drink recipes\n            cooked at an owned restaurant.\n            ",
            x_axis_name='Skill Level',
            y_axis_name='Food Final Quality Adder'))
    FINAL_QUALITY_STATE_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of final quality recipe states (Poor, Normal, Outstanding) to\n        the data associated with that recipe quality.\n        ',
        key_name='recipe_quality_state',
        key_type=TunableReference(
            description=
            '\n            The recipe quality state value.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.OBJECT_STATE),
            class_restrictions='ObjectStateValue',
            pack_safe=True),
        value_name='recipe_quality_state_value_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned recipe quality state value.\n            ',
            final_quality_to_customer_quality_multiplier=Tunable(
                description=
                '\n                This value is multiplied by the Recipe Difficulty To Customer\n                Quality Multiplier to determine the Customer Quality State value\n                of the recipe.\n                ',
                tunable_type=float,
                default=1),
            final_quality_to_customer_value_multiplier=Tunable(
                description=
                '\n                This value is multiplied by the Markup To Customer Value\n                Multiplier to determine the value of the Customer Value Stat\n                value of the recipe.\n                ',
                tunable_type=float,
                default=1)))
    PRICE_MARKUP_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of the current price markup of the restaurant to the data\n        associated with that markup.\n        ',
        key_name='markup_multiplier',
        key_type=Tunable(
            description=
            '\n            The markup multiplier. this needs to be in line with the available\n            markups tuned on the restaurant business.\n            ',
            tunable_type=float,
            default=1.5),
        value_name='markup_multiplier_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned markup multiplier.\n            ',
            markup_to_customer_value_multiplier=Tunable(
                description='\n                ',
                tunable_type=float,
                default=1)))
    BUSINESS_FUNDS_CATEGORY_FOR_COST_OF_INGREDIENTS = TunableEnumEntry(
        description=
        '\n        When a Chef cooks an order, the restaurant has to pay for the\n        ingredients. This is the category for those expenses.\n        ',
        tunable_type=BusinessFundsCategory,
        default=BusinessFundsCategory.NONE,
        invalid_enums=(BusinessFundsCategory.NONE, ))
    ATTIRE = TunableList(
        description=
        '\n        List of attires player can select to apply to the restaurant.\n        ',
        tunable=TunableEnumEntry(tunable_type=OutfitCategory,
                                 default=OutfitCategory.EVERYDAY,
                                 binary_type=EnumBinaryExportType.EnumUint32),
        export_modes=ExportModes.All)
    UNIFORM_CHEF_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit male chef uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_CHEF_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit female chef uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_WAITSTAFF_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit waiter uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_WAITSTAFF_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit waitress uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_HOST_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit male host uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_HOST_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit female host uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    RESTAURANT_VENUE = TunablePackSafeReference(
        description=
        '\n        This is a tunable reference to the type of Venue that will describe\n        a Restaurant. To be used for code references to restaurant venue types\n        in code.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.VENUE))
    HOST_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Host will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    WAITSTAFF_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Waiter will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    CHEF_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Chef will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    HOME_CHEF_SITUATION_TAG = TunableEnumWithFilter(
        description=
        '\n        Tag that we use on all the home chef situations.\n        ',
        tunable_type=Tag,
        filter_prefixes=['situation'],
        default=Tag.INVALID,
        invalid_enums=(Tag.INVALID, ),
        pack_safe=True)
    DINING_SITUATION_TAG = TunableEnumWithFilter(
        description=
        "\n        The tag used to find dining situations. \n        \n        This shouldn't need to be re-tuned after being set initially. If you\n        need to re-tune this you should probably talk to a GPE first.\n        ",
        tunable_type=Tag,
        filter_prefixes=['situation'],
        default=Tag.INVALID,
        pack_safe=True)
    TABLE_FOOD_SLOT_TYPE = TunableReference(
        description=
        '\n        The slot type of the food slot on the dining table.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE))
    TABLE_DRINK_SLOT_TYPE = TunableReference(
        description=
        '\n        The slot type of the drink slot on the dining table.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE))
    FOOD_AUTONOMY_PREFERENCE = TunableAutonomyPreference(
        description=
        '\n        The Autonomy Preference for the delivered food items.\n        ',
        is_scoring=False)
    DRINK_AUTONOMY_PREFERENCE = TunableAutonomyPreference(
        description=
        '\n        The Autonomy Preference for the delivered drink items.\n        ',
        is_scoring=False)
    CONSUMABLE_FULL_STATE_VALUE = TunableReference(
        description=
        '\n        The Consumable_Full state value. Food in restaurants will be set to\n        this value instead of defaulting to Consumable_Untouched to avoid other\n        Sims from eating your own food.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions=('ObjectStateValue', ))
    CONSUMABLE_EMPTY_STATE_VALUE = TunableReference(
        description=
        "\n        The Consumable_Empty state value. This is the state we'll use to\n        determine if food/drink is empty or not.\n        ",
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions=('ObjectStateValue', ))
    FOOD_DELIVERED_TO_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        "\n        The notification shown when the food is delivered to the player's table.\n        "
    )
    FOOD_STILL_ON_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        "\n        The notification that the player will see if the waitstaff try and\n        deliver food but there's still food on the table.\n        "
    )
    STAND_UP_INTERACTION = TunableReference(
        description=
        '\n        A reference to sim-stand so that sim-stand can be pushed on every sim\n        that is sitting at a table that is abandoned.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    DEFAULT_MENU = TunableEnumEntry(
        description=
        '\n        The default menu setting for a brand new restaurant.\n        ',
        tunable_type=MenuPresets,
        default=MenuPresets.CUSTOMIZE,
        export_modes=ExportModes.All,
        binary_type=EnumBinaryExportType.EnumUint32)
    SWITCH_SEAT_INTERACTION = TunableReference(
        description=
        '\n        This is a reference to the interaction that gets pushed on whichever Sim\n        is sitting in the seat that the Actor is switching to. The interaction \n        will be pushed onto the sseated Sim and will target the Actor Sims \n        current seat before the switch.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    RECOMMENDED_ORDER_INTERACTION = TunableReference(
        description=
        '\n        This is a reference to the interaction that will get pushed on the active Sim\n        to recommend orders to the Sim AFTER the having gone through the Menu UI.\n        \n        It will continue to retain the previous target.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION),
        pack_safe=True)
    INGREDIENT_PRICE_PERK_MAP = TunableMapping(
        description=
        '\n        Maps the various ingredient price perks with their corresponding\n        discount.\n        ',
        key_name='Ingredient Price Perk',
        key_type=TunableReference(
            description=
            '\n            A perk that gives a tunable multiplier to the price of ingredients\n            for restaurants.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK),
            pack_safe=True),
        value_name='Ingredient Price Multiplier',
        value_type=TunableRange(
            description=
            '\n            If the household has the corresponding perk, this value will be\n            multiplied by the final cost of each recipe to the restaurant.\n            ',
            tunable_type=float,
            default=1,
            minimum=0))
    CUSTOMERS_ORDER_EXPENSIVE_FOOD_PERK_DATA = TunableTuple(
        description=
        '\n        The perk that makes customers order more expensive food, and the off-lot\n        multiplier for that perk.\n        ',
        perk=TunablePackSafeReference(
            description=
            '\n            If the owning household has this perk, customers will pick two dishes to\n            order and then pick the most expensive of the two.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK)),
        off_lot_multiplier=TunableRange(
            description=
            '\n            When calculating off-lot profits, this is applied if the household\n            has this perk.\n            ',
            tunable_type=float,
            default=1.1,
            minimum=1))
    UNOWNED_RESTAURANT_PRICE_MULTIPLIER = TunableRange(
        description=
        '\n        The amount each item in the menu will be multiplied by on unowned\n        restaurant lots.\n        ',
        tunable_type=float,
        default=1.2,
        minimum=0,
        export_modes=ExportModes.All)
    CHEF_NOT_SKILLED_ENOUGH_THRESHOLD = Tunable(
        description=
        '\n        This is the value that a chef must reach when preparing a meal for a\n        customer without displaying the "Chef isn\'t skilled enough to make \n        receiver X" \n        \n        The number that must reach this value is the skill adder\n        of the chef and recipe difficulty adder.\n        ',
        tunable_type=int,
        default=-30)
    CHEF_NOT_SKILLED_ENOUGH_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification shown when the chef is working on a recipe that is \n        too difficult for their skill.\n        '
    )
    DEFAULT_PROFIT_PER_MEAL_FOR_OFF_LOT_SIMULATION = TunableRange(
        description=
        '\n        This is used as the default profit for a meal for off-lot simulation. Once\n        enough actual meals have been sold, this value becomes irrelevant and\n        the MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL tunable comes into use.\n        ',
        tunable_type=int,
        default=20,
        minimum=1)
    MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL = TunableRange(
        description=
        '\n        The number of meals to keep a running average of for the profit per meal\n        calculations during off lot simulations.\n        ',
        tunable_type=int,
        default=10,
        minimum=2)
    ADVERTISING_DATA_MAP = TunableMapping(
        description=
        '\n        The mapping between advertising type and the data for that type.\n        ',
        key_name='Advertising_Type',
        key_type=TunableEnumEntry(
            description='\n            The Advertising Type .\n            ',
            tunable_type=BusinessAdvertisingType,
            default=BusinessAdvertisingType.INVALID,
            invalid_enums=(BusinessAdvertisingType.INVALID, ),
            binary_type=EnumBinaryExportType.EnumUint32),
        value_name='Advertising_Data',
        value_type=TunableTuple(
            description=
            '\n            Data associated with this advertising type.\n            ',
            cost_per_hour=TunableRange(
                description=
                '\n                How much, per hour, it costs to use this advertising type.\n                ',
                tunable_type=int,
                default=10,
                minimum=0),
            customer_count_multiplier=TunableRange(
                description=
                '\n                This amount is multiplied by the ideal customer count for owned\n                restaurants.\n                ',
                tunable_type=float,
                default=0.8,
                minimum=0),
            ui_sort_order=TunableRange(
                description=
                '\n                Value representing how map entries will be sorted in the UI.\n                1 represents the first entry.  Avoid duplicate values\n                within the map.\n                ',
                tunable_type=int,
                minimum=1,
                default=1),
            export_class_name='RestaurantAdvertisingData'),
        tuple_name='RestaurantAdvertisingDataMapping',
        export_modes=ExportModes.All)
    TODDLER_SENT_TO_DAYCARE_FOR_RESTAURANTS = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification shown when a toddler is sent to daycare upon traveling\n        to a restaurant venue.\n        '
    )
    TIME_OF_DAY_TO_CUSTOMER_COUNT_MULTIPLIER_CURVE = TunableCurve(
        description=
        '\n        A curve that lets you tune a specific customer count multiplier\n        based on the time of day. Time of day should range between 0 and 23,\n        0 being midnight.\n        ',
        x_axis_name='time_of_day',
        y_axis_name='customer_count_multiplier')
class NeighborhoodPopulationService(Service):
    __qualname__ = 'NeighborhoodPopulationService'
    REGION_TO_HOUSEHOLD_POPULATION_DATA = TunableMapping(description='\n        Mapping of Region Description ID to household population data.  This is\n        used to fill households for the different type of regions.\n        ', key_name='Region Description', key_type=TunableRegionDescription(), value_name='Household Population Data', value_type=HouseholdPopulationData.TunableFactory())
    HOMELESS_HOUSEHOLD_TEMPLATES = TunableList(description='\n        A List of household templates that will be considered for homelesss\n        households.\n        ', tunable=TunableHouseholdTemplateWeightTuple())
    NUM_BEDS_TO_IDEAL_HOUSEHOLD_CURVE = TunableMapping(description='\n        Based on the number of beds and the number of sims in the household, a\n        multiplier will be applied to the household to determine if household\n        will be selected and added to zone.\n        ', key_name='Num Beds', key_type=Tunable(tunable_type=int, default=1), value_name='Ideal Household Curve', value_type=TunableCurve(x_axis_name='num_sim_in_household', y_axis_name='bonus_multiplier'))
    KID_TO_KID_BED_MULTIPLIER = TunableRange(description='\n        When trying to populate a lot if lot has a kids bed and household has a\n        kid in it.  This multiplier will be applied to the weight of household\n        when selecting household to move in.\n        ', tunable_type=float, default=1, minimum=1)
    SIGNIFICANT_OTHER_MULTIPLIER = TunableRange(description='\n        When trying to populate a lot and if lot has a double bed and household\n        contains a pair of sims that are considered significant other.  This\n        multiplier will be applied to the weight of household when selecting\n        household to move in.\n        ', tunable_type=float, default=1, minimum=1)

    def __init__(self):
        self._requests = []
        self._processing_element_handle = None

    def _process_population_request_gen(self, timeline):
        while self._requests:
            request = self._requests.pop(0)
            try:
                yield request.process_request_gen(timeline)
                request.process_completed(True)
            except GeneratorExit:
                raise
            except BaseException:
                request.process_completed(False)
                logger.exception('Exception raised while processing creating npc households')
            while self._requests:
                yield element_utils.run_child(timeline, element_utils.sleep_until_next_tick_element())
                continue
        self._processing_element_handle = None

    def add_population_request(self, num_to_fill, neighborhood_id, completion_callback, available_zone_ids, try_existing_households):
        account = self._get_account()
        if account is None:
            return False
        request = _FillZonePopulationRequest(account, num_to_fill, neighborhood_id, completion_callback, available_zone_ids=available_zone_ids, try_existing_households=try_existing_households)
        self._add_request(request)
        return True

    def add_homeless_household_request(self, num_to_fill, completion_callback):
        account = self._get_account()
        if account is None:
            return False
        request = _CreateHomelessHouseholdRequest(account, num_to_fill, None, completion_callback)
        self._add_request(request)
        return True

    def _get_account(self):
        client = services.client_manager().get_first_client()
        if client.account is not None or client.household is not None:
            return client.account

    @property
    def is_processing_requests(self):
        return self._processing_element_handle or len(self._requests) > 0

    def _add_request(self, request):
        self._requests.append(request)
        if self._processing_element_handle is None:
            timeline = services.time_service().sim_timeline
            element = elements.GeneratorElement(self._process_population_request_gen)
            self._processing_element_handle = timeline.schedule(element)
Esempio n. 20
0
class Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)):
    SKILL_LEVEL_LIST = TunableMapping(description='\n        A mapping defining the level boundaries for each skill type.\n        ', key_type=SkillLevelType, value_type=TunableList(description='\n            The level boundaries for skill type, specified as a delta from the\n            previous value.\n            ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n        Skill gain points based on skill effectiveness.\n        ', key_type=SkillEffectiveness, value_type=TunableCurve())
    DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n        Interval used when dynamic loot is used in a\n        PeriodicStatisticChangeElement.\n        ', tunable_type=float, default=1, minimum=1)
    INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n            The name of this skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n            The skill's normal description.\n            ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n            The skill description when it's locked.\n            ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n            Icon to be displayed for the Skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n            A list of icons to show in the tooltip of this\n            skill.\n            ', tunable=TunableIcon(description='\n                Icon that is displayed what types of objects help\n                improve this skill.\n                '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n            Tutorial instance for this skill. This will be used to bring up the\n            skill lesson from the first notification for Sim to know this skill.\n            ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n            Skill priority.  Higher priority skills will trump other skills when\n            being displayed on the UI. When a Sim gains multiple skills at the\n            same time, only the highest priority one will display a progress bar\n            over the Sim's head.\n            ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n            Tooltip which describes what the next level entails.\n            ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n            When this mood is set and active sim matches mood, the UI will\n            display a special effect on the skill bar to represent that this\n            skill is getting a bonus because of the mood.\n            ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n            If checked, this skill will be hidden.\n            ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n            Whether this skill will send update messages to the client\n            for non-active household sims (NPCs).\n            \n            e.g. A toddler's communication skill determines the VOX they use, so\n            the client needs to know the skill level for all toddlers in order\n            for this work properly.\n            ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n            Whether Sim will default has this skill.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n            Allowed ages for this skill.\n            ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n            A list of Vector2 points that define the desire curve for this\n            commodity.\n            ', tunable=TunableVector2(description='\n                Point on a Curve\n                ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n            The weight of the Skill with regards to autonomy.  It's ignored for\n            the purposes of sorting stats, but it's applied when scoring the\n            actual statistic operation for the SI.\n            ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n            Multipliers this skill applies to other statistics based on its\n            value.\n            ', key_type=TunableReference(description='\n                The statistic this multiplier will be applied to.\n                ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n                    Tunable curve where the X-axis defines the skill level, and\n                    the Y-axis defines the associated multiplier.\n                    ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n                    Direction where the multiplier should work on the\n                    statistic.  For example, a tuned decrease for an object's\n                    brokenness rate will not also increase the time it takes to\n                    repair it.\n                    ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n                    If checked, this modifier will look at the current\n                    effective skill value.  If unchecked, this modifier will\n                    look at the actual skill value.\n                    ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n            Multipliers this skill applies to the success chance of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n            Multipliers this skill applies to the monetary payout amount of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n            The associated categories of the skill\n            ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n            Skill level list to use.\n            ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n            Level-specific information, such as notifications to be displayed to\n            level up.\n            ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n                    The notification to display when the Sim obtains this level.\n                    The text will be provided two tokens: the Sim owning the\n                    skill and a number representing the 1-based skill level\n                    ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n                    Screen slam to show when reaches this skill level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n                    {1.String}, Skill Number - {2.Number}\n                    ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n                        The buff to place on a Sim when they reach this specific\n                        level of skill.\n                        ', manager=services.buff_manager())), rewards=TunableList(description='\n                    A reward to give for achieving this level.\n                    ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n                    A loot to apply for achieving this level.\n                    ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n                    Super affordances this adds to the Sim.\n                    ', tunable=TunableReference(description='\n                        A super affordance added to this Sim.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n                    Super affordances this adds to the target.\n                    ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n                    Mixers this adds to an associated actor object. (When targeting\n                    something else.)\n                    ', key_type=TunableReference(description='\n                        The super affordance these mixers are associated with.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n                        Set of mixer affordances associated with the super affordance.\n                        ', tunable=TunableReference(description='\n                            Linked mixer affordance.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n            Data used to modify the value of a new skill based on the level\n            of this skill.\n            \n            e.g. Toddler Communication skill transfers into Child Social skill.\n            ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n                    The new skill.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n                    A mapping between this skill's levels and the\n                    new skill's internal value.\n                    \n                    The keys are user facing skill levels.\n                    \n                    The values are the internal statistic value, not the user\n                    facing skill level.\n                    ", key_type=Tunable(description="\n                        This skill's level.\n                        \n                        This is the actual user facing skill level.\n                        ", tunable_type=int, default=0), value_type=Tunable(description='\n                        The new skill\'s value.\n                        \n                        This is the internal statistic\n                        value, not the user facing skill level."\n                        ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n            A list of skills that become unlocked when this skill is maxed.\n            ', tunable=TunableReference(description='\n                A skill to unlock.\n                ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n            If enabled, we associate this skill with a particular trend via tag\n            which you can find in trend_tuning.\n            ', tunable=TunableTag(description='\n                The trend tag we associate with this skill\n                ', filter_prefixes=('func_trend',)))}
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        self._skill_level_buff = None
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._max_level_update_sent = False

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        level_list = cls.get_level_list()
        cls.max_level = len(level_list)
        cls.min_value_tuning = 0
        cls.max_value_tuning = sum(level_list)
        cls._default_convergence_value = cls.min_value_tuning
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        for stat in cls.statistic_multipliers:
            multiplier = cls.statistic_multipliers[stat]
            curve = multiplier.curve
            direction = multiplier.direction
            use_effective_skill = multiplier.use_effective_skill
            stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill)
        for multiplier in cls.success_chance_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill)
        for multiplier in cls.monetary_payout_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill)

    @classmethod
    def _verify_tuning_callback(cls):
        success_multiplier_affordances = []
        for multiplier in cls.success_chance_multipliers:
            success_multiplier_affordances.extend(multiplier.affordance_list)
        if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')
        monetary_payout_multiplier_affordances = []
        for multiplier in cls.monetary_payout_multipliers:
            monetary_payout_multiplier_affordances.extend(multiplier.affordance_list)
        if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')

    @classproperty
    def skill_type(cls):
        return cls

    @constproperty
    def is_skill():
        return True

    @classproperty
    def autonomy_weight(cls):
        return cls.weight

    @constproperty
    def remove_on_convergence():
        return False

    @classproperty
    def valid_for_stat_testing(cls):
        return True

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if owner.age not in cls.ages:
            return False
        return super().can_add(owner, **kwargs)

    @classmethod
    def convert_to_user_value(cls, value):
        level_list = cls.get_level_list()
        if not level_list:
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(level_list):
            current_value -= level_threshold
            if current_value < 0:
                return level
        return level + 1

    @classmethod
    def convert_from_user_value(cls, user_value):
        (level_min, _) = cls._get_level_bounds(user_value)
        return level_min

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        skill_msg = Commodities_pb2.Skill_Update()
        skill_msg.skill_id = cls.guid64
        skill_msg.curr_points = int(stat_value)
        skill_msg.sim_id = sim_id
        return skill_msg

    @classmethod
    def get_level_list(cls):
        return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type)

    @classmethod
    def get_skill_effectiveness_points_gain(cls, effectiveness_level, level):
        skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level)
        if skill_gain_curve is not None:
            return skill_gain_curve.get(level)
        logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level)
        return 0

    def _get_level_data_for_skill_level(self, skill_level):
        level_data = self.level_data.get(skill_level)
        if level_data is None:
            logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level)
        return level_data

    @property
    def is_initial_value(self):
        return self.initial_value == self.get_value()

    def should_send_update(self, sim_info, stat_value):
        if sim_info.is_npc and not self.update_client_for_npcs:
            return False
        if self.hidden:
            return False
        if Skill.convert_to_user_value(stat_value) == 0:
            return False
        if self.reached_max_level:
            if self._max_level_update_sent:
                return False
            self._max_level_update_sent = True
        return True

    def on_initial_startup(self):
        super().on_initial_startup()
        skill_level = self.get_user_value()
        self._update_skill_level_buff(skill_level)

    def on_add(self):
        super().on_add()
        self._tracker.owner.add_modifiers_for_skill(self)
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is not None:
            provided_affordances = []
            for provided_affordance in level_data.target_super_affordances:
                provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self)
                provided_affordances.append(provided_affordance_data)
            self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances)
            self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers)
            sim = self._tracker._owner.get_sim_instance()
            apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances)

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()
        if not on_destroy:
            self._send_skill_delete_message()
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if not on_destroy:
            self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)

    def on_zone_load(self):
        self._max_level_update_sent = False

    def _apply_multipliers_to_continuous_statistics(self):
        for stat in self.statistic_multipliers:
            if stat.continuous:
                owner_stat = self.tracker.get_statistic(stat)
                if owner_stat is not None:
                    owner_stat._recalculate_modified_decay_rate()

    @classproperty
    def default_value(cls):
        return cls.initial_value

    @flexmethod
    @caches.cached
    def get_user_value(cls, inst):
        inst_or_cls = inst if inst is not None else cls
        return super(__class__, inst_or_cls).get_user_value()

    def _clear_user_value_cache(self):
        self.get_user_value.func.cache.clear()

    def set_value(self, value, *args, from_load=False, interaction=None, **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if from_load:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner
        new_value = self.get_value()
        new_level = self.convert_to_user_value(value)
        if old_value == self.initial_value or old_value != new_value:
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
        old_level = self.convert_to_user_value(old_value)
        if old_level < new_level or old_value == self.initial_value:
            self._apply_multipliers_to_continuous_statistics()
            event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def add_value(self, add_amount, interaction=None, **kwargs):
        old_value = self.get_value()
        if old_value == self.initial_value:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
        else:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION
        super().add_value(add_amount, interaction=interaction)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if interaction is not None:
            interaction_name = interaction.affordance.__name__
        else:
            interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE
        self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name)

    def _update_value(self):
        old_value = self._value
        if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled:
            last_update = self._last_update
        time_delta = super()._update_value()
        if not caches.skip_cache:
            self._clear_user_value_cache()
        new_value = self._value
        if old_value < new_value:
            event_manager = services.get_event_manager()
            sim_info = self._tracker._owner if self._tracker is not None else None
            if old_value == self.initial_value:
                telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
                self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE)
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
            old_level = self.convert_to_user_value(old_value)
            new_level = self.convert_to_user_value(new_value)
            if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim:
                gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update)
            if old_level < new_level or old_value == self.initial_value:
                if self._tracker is not None:
                    self._tracker.notify_watchers(self.stat_type, self._value, self._value)
                event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        if not self.reached_max_level:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner if self._tracker is not None else None
        event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def on_skill_updated(self, telemhook, old_value, new_value, affordance_name):
        owner_sim_info = self._tracker._owner
        if owner_sim_info.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook:
                hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
                hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name)
                hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True)
                hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value)
        if old_value == self.initial_value:
            skill_level = self.convert_to_user_value(old_value)
            self._handle_skill_up(skill_level)

    def _send_skill_delete_message(self):
        if self.tracker.owner.is_npc:
            return
        skill_msg = Commodities_pb2.SkillDelete()
        skill_msg.skill_id = self.guid64
        op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg)
        Distributor.instance().add_op(self.tracker.owner, op)

    @staticmethod
    def _callback_handler(stat_inst):
        new_level = stat_inst.get_user_value()
        old_level = new_level - 1
        stat_inst.on_skill_level_up(old_level, new_level)
        stat_inst.refresh_threshold_callback()

    def _handle_skill_up(self, skill_level):
        self._show_level_notification(skill_level)
        self._update_skill_level_buff(skill_level)
        self._try_give_skill_up_payout(skill_level)
        self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)
        super_affordances = tuple(self._tracker.get_cached_super_affordances_gen())
        apply_super_affordance_commodity_flags(sim, self, super_affordances)

    def _recalculate_modified_decay_rate(self):
        pass

    def refresh_level_up_callback(self):
        self._destory_callback_handle()

        def _on_level_up_callback(stat_inst):
            new_level = stat_inst.get_user_value()
            old_level = new_level - 1
            stat_inst.on_skill_level_up(old_level, new_level)
            stat_inst.refresh_level_up_callback()

        self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback)

    def on_skill_level_up(self, old_level, new_level):
        tracker = self.tracker
        sim_info = tracker._owner
        if self.reached_max_level:
            for skill in self.skill_unlocks_on_max:
                skill_instance = tracker.add_statistic(skill, force_add=True)
                skill_instance.set_value(skill.initial_value)
        with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        self._handle_skill_up(new_level)
        services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def _show_level_notification(self, skill_level, ignore_npc_check=False):
        sim_info = self._tracker._owner
        if not (ignore_npc_check or not sim_info.is_npc):
            if skill_level == 1:
                tutorial_service = services.get_tutorial_service()
                if tutorial_service is not None and tutorial_service.is_tutorial_running():
                    return
            level_data = self._get_level_data_for_skill_level(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None:
                    if skill_level == 1:
                        tutorial_id = self.tutorial.guid64
                notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info))
                notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id)
                if level_data.level_up_screen_slam is not None:
                    level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level)

    def _update_skill_level_buff(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        new_buff = level_data.skill_level_buff if level_data is not None else None
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if new_buff is not None:
            self._skill_level_buff = self._tracker.owner.add_buff(new_buff)

    def _try_give_skill_up_payout(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        if level_data is None:
            return
        if level_data.rewards:
            for reward in level_data.rewards:
                reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self)
        if level_data.loot:
            resolver = SingleSimResolver(self._tracker.owner)
            for loot in level_data.loot:
                loot.apply_to_resolver(resolver)

    def force_show_level_notification(self, skill_level):
        self._show_level_notification(skill_level, ignore_npc_check=True)

    @classmethod
    def send_commodity_update_message(cls, sim_info, old_value, new_value):
        stat_instance = sim_info.get_statistic(cls.stat_type, add=False)
        if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value):
            return
        msg = cls.create_skill_update_msg(sim_info.id, new_value)
        add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False)
        change_rate = stat_instance.get_change_rate()
        hide_progress_bar = False
        if sim_info.is_npc or sim_info.is_skill_bar_suppressed():
            hide_progress_bar = True
        op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar)
        distributor.ops.record(sim_info, op)

    def save_statistic(self, commodities, skills, ranked_stats, tracker):
        current_value = self.get_saved_value()
        if current_value == self.initial_value:
            return
        message = protocols.Skill()
        message.name_hash = self.guid64
        message.value = current_value
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks()
        skills.append(message)

    def unlocks_skills_on_max(self):
        return True

    def can_decay(self):
        return False

    def get_skill_provided_affordances(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return ((), ())
        return (level_data.super_affordances, level_data.target_super_affordances)

    def get_skill_provided_actor_mixers(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return
        return level_data.actor_mixers

    def get_actor_mixers(self, super_interaction):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return []
        mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else []
        return mixers

    @flexmethod
    def populate_localization_token(cls, inst, token):
        inst_or_cls = inst if inst is not None else cls
        token.type = LocalizedStringToken.STRING
        token.text_string = inst_or_cls.stat_name
Esempio n. 21
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)
Esempio n. 22
0
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