Esempio n. 1
0
class SlotObjectsFromInventory(XevtTriggeredElement, HasTunableFactory,
                               AutoFactoryInit):
    FACTORY_TUNABLES = {
        'description':
        '\n            Transfer particpant objects into the target object available slots\n            of the tuned slot type. \n            ',
        'slot_strategy':
        SlotStrategyVariant(
            description=
            '\n            The slot strategy we want to use to place objects from the transfer\n            source into slots on the target.\n            '
        ),
        'slot_failure_notification':
        OptionalTunable(
            description=
            '\n            If enabled, we will show a notification to the player when this\n            element runs and no objects are successfully slotted.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                Notification to show if we fail to slot any objects.\n                '
            ))
    }

    def _do_behavior(self):
        slot_strategy = self.slot_strategy(self.interaction.get_resolver())
        if not slot_strategy.slot_objects(
        ) and self.slot_failure_notification is not None:
            dialog = self.slot_failure_notification(
                self.interaction.sim, resolver=self.interaction.get_resolver())
            dialog.show_dialog()
        return True
Esempio n. 2
0
class LandlordTuning:
    LANDLORD_FILTER = TunableSimFilter.TunablePackSafeReference(
        description=
        '\n        The Sim Filter used to find/create a Landlord for the game.\n        '
    )
    LANDLORD_REL_BIT = RelationshipBit.TunablePackSafeReference(
        description=
        '\n        The rel bit to add between a landlord and apartment tenants. This will\n        be removed if a tenant moves out of an apartment.\n        '
    )
    TENANT_REL_BIT = RelationshipBit.TunablePackSafeReference(
        description=
        '\n        The rel bit to add between an apartment Tenant and their Landlord. This\n        will be removed if a tenant moves out of an apartment.\n        '
    )
    LANDLORD_TRAIT = Trait.TunablePackSafeReference(
        description=
        '\n        The Landlord Trait used in testing and Sim Filters.\n        '
    )
    LANDLORD_FIRST_PLAY_RENT_REMINDER_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification to show a household if they are played on a new\n        apartment home.\n        '
    )
    HOUSEHOLD_LANDLORD_EXCEPTION_TESTS = TunableTestSet(
        description=
        '\n        Tests to run when determining if a household requires a landlord.\n        '
    )
Esempio n. 3
0
class EndSkeleton(CommonInteractionCompletedSituationState):
    FACTORY_TUNABLES = {
        'leaving_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that is shown when a summoned Skeleton NPC leaves.\n            '
        )
    }

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

    def _on_interaction_of_interest_complete(self, **kwargs):
        skeleton = self.owner.get_skeleton()
        resolver = SingleSimResolver(skeleton.sim_info)
        leaving_notification = self.leaving_notification(skeleton,
                                                         resolver=resolver)
        leaving_notification.show_dialog()
        self.owner._self_destruct()

    def _additional_tests(self, sim_info, event, resolver):
        skeleton = self.owner.get_skeleton()
        if skeleton is not None and self.owner.get_skeleton(
        ).sim_info is sim_info:
            return True
        return False
Esempio n. 4
0
class NotificationElement(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'description':
        "Show a notification to a Sim's player.",
        'recipient_subject':
        TunableEnumEntry(
            description=
            "\n            The Sim's whose player will be the recipient of this notification.\n            ",
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'limit_to_one_notification':
        Tunable(
            description=
            '\n            If checked, this notification will only be displayed for the first\n            recipient subject. This is useful to prevent duplicates of the\n            notification from showing up when sending a notification to\n            LotOnwers or other Participant Types that have multiple Sims.\n            ',
            tunable_type=bool,
            default=False),
        'dialog':
        TunableTestedVariant(
            tunable_type=TunableUiDialogNotificationSnippet()),
        'allow_autonomous':
        Tunable(
            description=
            '\n            If checked, then this notification will be displayed even if its\n            owning interaction was initiated by autonomy. If unchecked, then the\n            notification is suppressed if the interaction is autonomous.\n            ',
            tunable_type=bool,
            default=True)
    }

    def _do_behavior(self, *args, **kwargs):
        return self.show_notification(*args, **kwargs)

    def show_notification(self, recipients=DEFAULT, **kwargs):
        if not self.allow_autonomous and self.interaction.is_autonomous:
            return
        if recipients is DEFAULT:
            if self.recipient_subject == ParticipantType.ActiveHousehold:
                recipients = (services.active_sim_info(), )
            else:
                recipients = self.interaction.get_participants(
                    self.recipient_subject)
        simless = self.interaction.simless
        for recipient in recipients:
            if not simless:
                if recipient.is_selectable:
                    resolver = self.interaction.get_resolver()
                    dialog = self.dialog(recipient, resolver=resolver)
                    if dialog is not None:
                        dialog.show_dialog(**kwargs)
                        if self.limit_to_one_notification:
                            break
            resolver = self.interaction.get_resolver()
            dialog = self.dialog(recipient, resolver=resolver)
            if dialog is not None:
                dialog.show_dialog(**kwargs)
                if self.limit_to_one_notification:
                    break
Esempio n. 5
0
class AwardPerkLoot(BaseLootOperation):
    FACTORY_TUNABLES = {
        'description':
        '\n            This loot will give the specified perk to the sim.\n            ',
        'perk':
        TunableReference(
            description=
            '\n            The perk to give the Sim. \n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK)),
        'award_strategy':
        TunableVariant(unlock=_UnlockPerkStrategy.TunableFactory(),
                       progress=_PerkProgressStrategy.TunableFactory(),
                       default='unlock'),
        'notification_on_successful_unlock':
        OptionalTunable(
            description=
            '\n            If enabled, a notification that displays when the perk is\n            successfully awarded.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                This is the notification that shows when the perk is successfully\n                unlocked.\n                '
            ))
    }

    def __init__(self,
                 perk,
                 award_strategy=None,
                 notification_on_successful_unlock=None,
                 **kwargs):
        super().__init__(**kwargs)
        self._perk = perk
        self._award_strategy = award_strategy
        self._notification_on_successful_unlock = notification_on_successful_unlock

    def _apply_to_subject_and_target(self, subject, target, resolver):
        if subject is None:
            logger.error('Subject {} is None for the loot {}.', self.subject,
                         self)
            return False
        bucks_tracker = BucksUtils.get_tracker_for_bucks_type(
            self._perk.associated_bucks_type, subject.sim_id, add_if_none=True)
        if bucks_tracker is None:
            logger.error(
                "Subject {} doesn't have a perk bucks tracker of type {} for the loot {}.",
                self.subject, self._bucks_type, self)
            return False
        show_dialog = self._award_strategy.try_unlock_perk(
            subject, bucks_tracker, self._perk)
        if show_dialog and self._notification_on_successful_unlock is not None:
            notification = self._notification_on_successful_unlock(
                subject, resolver=SingleSimResolver(subject))
            notification.show_dialog()
Esempio n. 6
0
class DaycareTuning:
    NANNY_SERVICE_NPC = TunableReference(description='\n        The nanny service NPC. We check if this is hired to take \n        away babies on sims leaving.\n        ', manager=services.get_instance_manager(sims4.resources.Types.SERVICE_NPC))
    BUTLER_SERVICE_NPC = TunablePackSafeReference(description='\n        The butler service NPC. If selected to look after children, the butler\n        should have similar effects as the nanny with regards to Daycare.\n        ', manager=services.get_instance_manager(sims4.resources.Types.SERVICE_NPC))
    NANNY_SERVICE_NPC_DIALOG = UiDialogOkCancel.TunableFactory(description='\n        A dialog that shows up when toddlers (not babies) are left home alone\n        requiring daycare. If the player selects Ok, a Nanny NPC is hired for\n        the duration of daycare, and the player can keep playing with their\n        toddlers. If Cancel is selected, regular daycare behavior kicks in and\n        the toddlers become uncontrollable.\n        ')
    DAYCARE_TRAIT_ON_KIDS = TunableReference(description='\n        The trait that indicates a baby is at daycare.\n        ', manager=services.trait_manager())
    NANNY_TRAIT_ON_KIDS = TunableReference(description='\n        The trait that children and babies that are with the nanny have.\n        ', manager=services.trait_manager())
    SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single baby is sent to daycare. You can\n        reference this single baby by name.\n        ')
    SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple babies are sent to daycare. You can\n        not reference any of these babies by name.\n        ')
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single baby is brought back from daycare.\n        You can reference this single baby by name.\n        ')
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple babies are brought back from\n        daycare. You can not reference any of these babies by name.\n        ')
    SEND_CHILD_TO_NANNY_NOTIFICATION_SINGLE = TunableUiDialogNotificationSnippet(description='\n        The message appears when a single child is sent to the nanny. You can\n        reference this single nanny by name.\n        ')
    SEND_CHILD_TO_NANNY_NOTIFICATION_MULTIPLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple children are sent to the nanny. You\n        can not reference any of these children by name.\n        ')
    BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_SINGLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when a single child is brought back from the\n        nanny. You can reference this single child by name.\n        ')
    BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_MULTIPLE = TunableUiDialogNotificationSnippet(description='\n        The message appearing when multiple children are brought back from\n        the nanny. You can not reference any of these by name.\n        ')
    GO_TO_DAYCARE_INTERACTION = TunableReference(description='\n        An interaction to push on instantiated Sims that need to go to Daycare.\n        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))
    DAYCARE_AWAY_ACTIONS = TunableMapping(description='\n        Map of commodities to away action.  When the default away action is\n        asked for we look at the ad data of each commodity and select the away\n        action linked to the commodity that is advertising the highest.\n        \n        This set of away actions is used exclusively for Sims in daycare.\n        ', key_type=TunableReference(description='\n            The commodity that we will look at the advertising value for.\n            ', manager=services.get_instance_manager(Types.STATISTIC), class_restrictions=('Commodity',)), value_type=TunableReference(description='\n            The away action that will applied if the key is the highest\n            advertising commodity of the ones listed.\n            ', manager=services.get_instance_manager(Types.AWAY_ACTION)))
Esempio n. 7
0
class LoanTunables:
    DEBT_STATISTIC = TunableReference(description='\n        The statistic used to track the amount of debt this Sim has incurred.\n        ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Statistic',))
    DEATH_DEBT_COLLECTION_NOTIFICATION = TunableUiDialogNotificationSnippet(description='\n        The notification shown when a Sim that has unpaid debt dies.\n        ')
    POVERTY_LOOT = TunablePackSafeReference(description='\n        A loot action applied to all other members of the household if a Sim\n        with unpaid dies, and the debt amount is greater than or equal to\n        the household funds.\n        ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))
    INTEREST_MAP = TunableMapping(description='\n        Mapping between loan type and the interest rate for that type.\n        ', key_type=TunableEnumEntry(description='\n            The type of loan taken.\n            ', tunable_type=LoanType, default=LoanType.INVALID, invalid_enums=(LoanType.INVALID,)), value_type=TunablePercent(description='\n            The interest rate for the corresponding loan type.\n            ', default=10), tuple_name='InterestMappingTuple', export_modes=ExportModes.All)

    @staticmethod
    def get_loan_amount(amount, loan_type):
        interest_rate = LoanTunables.INTEREST_MAP.get(loan_type, 0)
        amount += amount*interest_rate
        return int(amount)

    @staticmethod
    def add_debt(sim_info, amount):
        if amount == 0:
            return
        sim_info_debt_stat = sim_info.statistic_tracker.get_statistic(LoanTunables.DEBT_STATISTIC, add=True)
        sim_info_debt_stat.add_value(amount)
        LoanTunables.send_loan_op(sim_info, -amount)

    @staticmethod
    def send_loan_op(sim_info, amount):
        msg = Sims_pb2.SetLoan()
        msg.amount = amount
        op = GenericProtocolBufferOp(Operation.SET_LOAN, msg)
        Distributor.instance().add_op(sim_info, op)

    @staticmethod
    def on_death(sim_info):
        debt_stat = sim_info.statistic_tracker.get_statistic(LoanTunables.DEBT_STATISTIC)
        if debt_stat is None:
            return
        debt_amount = debt_stat.get_value()
        if debt_amount == 0:
            return
        resolver = SingleSimResolver(sim_info)
        dialog = LoanTunables.DEATH_DEBT_COLLECTION_NOTIFICATION(sim_info, resolver=resolver)
        dialog.show_dialog()
        household_funds = sim_info.household.funds.money
        if LoanTunables.POVERTY_LOOT is not None:
            if debt_amount >= household_funds:
                for hh_sim_info in sim_info.household.sim_info_gen():
                    if sim_info is hh_sim_info:
                        continue
                    resolver = DoubleSimResolver(hh_sim_info, sim_info)
                    LoanTunables.POVERTY_LOOT.apply_to_resolver(resolver)
        amount_to_remove = min(debt_amount, household_funds)
        sim_info.household.funds.try_remove_amount(amount_to_remove, Consts_pb2.TELEMETRY_LOANS_SIM_DEATH)
class _NeighborWaitToBeGreetedState(CommonInteractionCompletedSituationState):
    FACTORY_TUNABLES = {
        'early_exit_loot':
        TunableList(
            description=
            '\n            A list of loot to apply between the neighbor and the active\n            household Sims if this stiuation state times out.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                A loot action applied to all of the active household Sims if this\n                situation state times out.\n                '
            )),
        'early_exit_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            Notification that will be shown when this situation state times\n            out.\n            '
        )
    }

    def __init__(self,
                 *args,
                 early_exit_loot=tuple(),
                 early_exit_notification=None,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self._early_exit_loot = early_exit_loot
        self._early_exit_notification = early_exit_notification

    def _on_interaction_of_interest_complete(self, **kwargs):
        self._change_state(self.owner._hangout_state())

    def timer_expired(self):
        for sim_info in services.active_household():
            resolver = DoubleSimResolver(sim_info,
                                         self.owner._neighbor_sim.sim_info)
            for loot_action in self._early_exit_loot:
                loot_action.apply_to_resolver(resolver)
        resolver = DoubleSimResolver(services.active_sim_info(),
                                     self.owner._neighbor_sim.sim_info)
        early_exit_notification = self._early_exit_notification(
            services.active_sim_info(), resolver=resolver)
        early_exit_notification.show_dialog()
        self.owner._self_destruct()
Esempio n. 9
0
class SetGoodbyeNotificationElement(XevtTriggeredElement):
    __qualname__ = 'SetGoodbyeNotificationElement'
    NEVER_USE_NOTIFICATION_NO_MATTER_WHAT = 'never_use_notification_no_matter_what'
    FACTORY_TUNABLES = {
        'description':
        'Set the notification that a Sim will display when they leave.',
        'participant':
        TunableEnumEntry(
            description=
            '\n            The participant of the interaction who will have their "goodbye"\n            notification set.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'goodbye_notification':
        TunableVariant(
            description=
            '\n                The "goodbye" notification that will be set on this Sim. This\n                notification will be displayed when this Sim leaves the lot\n                (unless it gets overridden later).\n                ',
            notification=TunableUiDialogNotificationSnippet(),
            locked_args={
                'no_notification':
                None,
                'never_use_notification_no_matter_what':
                NEVER_USE_NOTIFICATION_NO_MATTER_WHAT
            },
            default='no_notification'),
        'only_set_if_notification_already_set':
        Tunable(
            description=
            "\n                If the Sim doesn't have a goodbye notification already set and\n                this checkbox is checked, leave the goodbye notification unset.\n                ",
            tunable_type=bool,
            default=True)
    }

    def _do_behavior(self):
        participants = self.interaction.get_participants(self.participant)
        for participant in participants:
            if participant.sim_info.goodbye_notification == self.NEVER_USE_NOTIFICATION_NO_MATTER_WHAT:
                pass
            if participant.sim_info.goodbye_notification is None and self.only_set_if_notification_already_set:
                pass
            participant.sim_info.goodbye_notification = self.goodbye_notification
Esempio n. 10
0
class CooldownFestivalState(TimedFestivalState):
    FACTORY_TUNABLES = {
        'notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that will appear when we enter this festival\n            state.\n            '
        )
    }

    @classproperty
    def key(cls):
        return 5

    def on_state_activated(self, reader=None, preroll_time_override=None):
        super().on_state_activated(reader=reader,
                                   preroll_time_override=preroll_time_override)
        for situation in self._owner.get_running_festival_situations():
            situation.put_on_cooldown()
        notification = self.notification(services.active_sim_info())
        notification.show_dialog()

    def _get_next_state(self):
        return self._owner.cleanup_festival_state(self._owner)
Esempio n. 11
0
class FestivalContestSubmitElement(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'success_notification_by_rank':
        TunableList(
            description=
            '\n            Notifications displayed if submitted object is large enough to be ranked in\n            the contest. Index refers to the place that the player is in currently.\n            1st, 2nd, 3rd, etc.\n            ',
            tunable=UiDialogNotification.TunableFactory(),
            tuning_group=GroupNames.UI),
        'unranked_notification':
        OptionalTunable(
            description=
            '\n            If enabled, notification displayed if submitted object is not large enough to rank in\n            the contest. \n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification that will appear when the submitted object does not rank.\n                '
            ),
            tuning_group=GroupNames.UI)
    }

    def _do_behavior(self):
        resolver = self.interaction.get_resolver()
        running_contests = services.drama_scheduler_service(
        ).get_running_nodes_by_drama_node_type(DramaNodeType.FESTIVAL)
        for contest in running_contests:
            if hasattr(contest, 'festival_contest_tuning'):
                if contest.festival_contest_tuning is None:
                    continue
                if contest.is_during_pre_festival():
                    continue
                obj = self.interaction.get_participant(
                    ParticipantType.PickedObject)
                if obj is None:
                    logger.error('{} does not have PickedObject participant',
                                 resolver)
                    return False
                sim = self.interaction.sim
                if sim is None:
                    logger.error('{} does not have sim participant', resolver)
                    return False
                return self._enter_object_into_contest(contest, sim, obj,
                                                       resolver)
        logger.error('{} no valid active Contest', resolver)
        return False

    def _enter_object_into_contest(self, contest, sim, obj, resolver):
        weight_statistic = contest.festival_contest_tuning._weight_statistic
        weight_tracker = obj.get_tracker(weight_statistic)
        if weight_tracker is None:
            logger.error('{} picked object does not have weight stat {}',
                         resolver, weight_statistic)
            return False
        if contest.festival_contest_tuning._destroy_object_on_submit and not self._destroy_object(
                contest, sim, obj, resolver):
            return False
        elif not self._add_score(contest, sim, obj, resolver):
            return False
        return True

    def _destroy_object(self, contest, sim, obj, resolver):
        obj.make_transient()
        return True

    def _add_score(self, contest, sim, obj, resolver):
        weight_statistic = contest.festival_contest_tuning._weight_statistic
        weight_tracker = obj.get_tracker(weight_statistic)
        if weight_tracker is None:
            logger.error('{} picked object does not have weight stat {}',
                         resolver, weight_statistic)
            return False
        rank = contest.add_score(sim.id, obj.id,
                                 weight_tracker.get_value(weight_statistic))
        if rank is not None:
            if rank >= len(self.success_notification_by_rank):
                return False
            notification = self.success_notification_by_rank[rank]
            dialog = notification(sim, target_sim_id=sim.id, resolver=resolver)
            dialog.show_dialog()
        elif self.unranked_notification is not None:
            dialog = self.unranked_notification(sim,
                                                target_sim_id=sim.id,
                                                resolver=resolver)
            dialog.show_dialog()
        return True
Esempio n. 12
0
class RelationshipBit(HasTunableReference,
                      SuperAffordanceProviderMixin,
                      MixerProviderMixin,
                      metaclass=HashedTunedInstanceMetaclass,
                      manager=services.relationship_bit_manager()):
    INSTANCE_TUNABLES = {
        'display_name':
        TunableLocalizedStringFactory(
            description=
            '\n            Localized name of this bit\n            ',
            allow_none=True,
            export_modes=ExportModes.All),
        'bit_description':
        TunableLocalizedStringFactory(
            description=
            '\n            Localized description of this bit\n            ',
            allow_none=True,
            export_modes=ExportModes.All),
        'icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed for the relationship bit.\n            ',
            allow_none=True,
            resource_types=CompoundTypes.IMAGE,
            export_modes=ExportModes.All),
        'bit_added_notification':
        OptionalTunable(
            description=
            '\n            If enabled, a notification will be displayed when this bit is added.\n            ',
            tunable=TunableTuple(
                notification=TunableUiDialogNotificationSnippet(),
                show_if_unselectable=Tunable(
                    description=
                    '\n                    If this is checked, then the notification is displayed if\n                    the owning Sim is not selectable, but the target is.\n                    Normally, notifications are only displayed if the owning Sim\n                    is selectable.\n                    ',
                    tunable_type=bool,
                    default=False))),
        'bit_removed_notification':
        OptionalTunable(
            description=
            '\n            If enabled, a notification will be displayed when this bit is removed.\n            ',
            tunable=TunableUiDialogNotificationSnippet()),
        'depth':
        Tunable(
            description=
            '\n            The amount of depth provided by the bit.\n            ',
            tunable_type=int,
            default=0),
        'priority':
        Tunable(
            description=
            '\n            Priority of the bit.  This is used when a bit turns on while a\n            mutually exclusive bit is already on.\n            ',
            tunable_type=float,
            default=0),
        'display_priority':
        Tunable(
            description=
            '\n            The priority of this bit with regards to UI.  Only the highest\n            priority bits are displayed.\n            ',
            tunable_type=int,
            default=0,
            export_modes=ExportModes.All),
        'exclusive':
        Tunable(
            description=
            "\n            Whether or not the bit is exclusive. This means that a sim can only have \n            this bit with one other sim.  If you attempt to add an exclusive bit to \n            a sim that already has the same one with another sim, it will remove the \n            old bit.\n            \n            Example: A sim can only be BFF's with one other sim.  If the sim asks \n            another sim to be their BFF, the old bit is removed.\n            ",
            tunable_type=bool,
            default=False),
        'visible':
        Tunable(
            description=
            "\n            If True, this bit has the potential to be visible when applied,\n            depending on display_priority and the other active bits.  If False,\n            the bit will not be displayed unless it's part of the\n            REL_INSPECTOR_TRACK bit track.\n            ",
            tunable_type=bool,
            default=True),
        'group_id':
        TunableEnumEntry(
            description=
            '\n            The group this bit belongs to.  Two bits of the same group cannot\n            belong in the same set of bits for a given relationship.\n            ',
            tunable_type=RelationshipBitType,
            default=RelationshipBitType.NoGroup),
        'triggered_track':
        TunableReference(
            description=
            '\n            If set, the track that is triggered when this bit is set\n            ',
            manager=services.statistic_manager(),
            allow_none=True,
            class_restrictions='RelationshipTrack'),
        'required_bits':
        TunableList(
            description=
            '\n            List of all bits that are required to be on in order to allow this\n            bit to turn on.\n            ',
            tunable=TunableReference(services.relationship_bit_manager())),
        'timeout':
        TunableSimMinute(
            description=
            '\n            The length of time this bit will last in sim minutes.  0 means the\n            bit will never timeout.\n            ',
            default=0),
        'remove_on_threshold':
        OptionalTunable(tunable=TunableTuple(
            description=
            '\n                If enabled, this bit will be removed when the referenced track\n                reaches the appropriate threshold.\n                ',
            track=TunableReference(
                description=
                '\n                    The track to be tested.\n                    ',
                manager=services.statistic_manager(),
                class_restrictions='RelationshipTrack'),
            threshold=TunableThreshold(
                description=
                '\n                    The threshold at which to remove this bit.\n                    '
            ))),
        'historical_bits':
        OptionalTunable(tunable=TunableList(tunable=TunableTuple(
            age_trans_from=
            TunableEnumEntry(description=
                             '\n                        Age we are transitioning out of.\n                        ',
                             tunable_type=sims.sim_info_types.Age,
                             default=sims.sim_info_types.Age.CHILD),
            new_historical_bit=TunableReference(
                description=
                '\n                        New historical bit the sim obtains\n                        ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.RELATIONSHIP_BIT))))),
        'collection_ids':
        TunableList(tunable=TunableEnumEntry(
            description=
            '\n                The bit collection id this bit belongs to, like family,\n                friends, romance. Default to be All.\n                ',
            tunable_type=RelationshipBitCollectionUid,
            default=RelationshipBitCollectionUid.All,
            export_modes=ExportModes.All)),
        'buffs_on_add_bit':
        TunableList(tunable=TunableTuple(
            buff_ref=buffs.tunable.
            TunableBuffReference(description=
                                 '\n                    Buff that gets added to sim when bit is added.\n                    '
                                 ),
            amount=Tunable(
                description=
                '\n                    If buff is tied to commodity the amount to add to the\n                    commodity.\n                    ',
                tunable_type=float,
                default=1),
            only_add_once=Tunable(
                description=
                '\n                    If True, the buff should only get added once no matter how\n                    many times this bit is being applied.\n                    ',
                tunable_type=bool,
                default=False))),
        'buffs_to_add_if_on_active_lot':
        TunableList(
            description=
            "\n            List of buffs to add when a sim that I share this relationship with\n            is in the household that owns the lot that I'm on.\n            ",
            tunable=buffs.tunable.TunableBuffReference(
                description=
                '\n                Buff that gets added to sim when bit is added.\n                '
            )),
        'autonomy_multiplier':
        Tunable(
            description=
            '\n            This value is multiplied to the autonomy score of any interaction\n            performed between the two Sims.  For example, when the Sim decides\n            to socialize, she will start looking at targets to socialize with.\n            If there is a Sim who she shares this bit with, her final score for\n            socializing with that Sim will be multiplied by this value.\n            ',
            tunable_type=float,
            default=1),
        'relationship_culling_prevention':
        TunableEnumEntry(
            description=
            '\n            Determine if bit should prevent relationship culling.  \n            \n            ALLOW_ALL = all culling\n            PLAYED_ONLY = only cull if not a played household\n            PLAYED_AND_UNPLAYED = disallow culling for played and unplayed sims. (e.g. family bits)\n            ',
            tunable_type=RelationshipBitCullingPrevention,
            default=RelationshipBitCullingPrevention.ALLOW_ALL),
        'persisted_tuning':
        Tunable(
            description=
            '\n            Whether this bit will persist when saving a Sim. \n            \n            For example, a Sims is good_friends should be set to true, but\n            romantic_gettingMarried should not be saved.\n            ',
            tunable_type=bool,
            default=True),
        'bit_added_loot_list':
        TunableList(
            description=
            '\n            A list of loot operations to apply when this relationship bit is\n            added.\n            ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.ACTION),
                                     class_restrictions=('LootActions', ),
                                     pack_safe=True)),
        'directionality':
        TunableEnumEntry(
            description=
            '\n            The direction that this Relationship bit points.  Bidirectional\n            means that both Sims will be given this bit if it is added.\n            Unidirectional means that only one Sim will be given this bit.\n            If it is coming from loot that bit will be given to the Actor.\n            ',
            tunable_type=RelationshipDirection,
            default=RelationshipDirection.BIDIRECTIONAL)
    }
    is_track_bit = False
    trait_replacement_bits = None
    _cached_commodity_flags = None

    def __init__(self):
        self._buff_handles = None
        self._conditional_removal_listener = None
        self._appropriate_buffs_handles = None

    @classproperty
    def persisted(cls):
        return cls.persisted_tuning

    @classproperty
    def is_collection(cls):
        return False

    def add_buffs_for_bit_add(self, sim, relationship, from_load):
        for buff_data in self.buffs_on_add_bit:
            buff_type = buff_data.buff_ref.buff_type
            if from_load and buff_type.commodity:
                continue
            if buff_data.only_add_once:
                if buff_type.guid64 in relationship.get_bit_added_buffs(
                        sim.sim_id):
                    continue
                relationship.add_bit_added_buffs(sim.sim_id, buff_type)
            if buff_type.commodity:
                tracker = sim.get_tracker(buff_type.commodity)
                tracker.add_value(buff_type.commodity, buff_data.amount)
                sim.set_buff_reason(buff_type, buff_data.buff_ref.buff_reason)
            else:
                buff_handle = sim.add_buff(
                    buff_type, buff_reason=buff_data.buff_ref.buff_reason)
                if self._buff_handles is None:
                    self._buff_handles = []
                self._buff_handles.append((sim.sim_id, buff_handle))

    def _apply_bit_added_loot(self, sim_info, target_sim_info):
        resolver = DoubleSimResolver(sim_info, target_sim_info)
        for loot in self.bit_added_loot_list:
            loot.apply_to_resolver(resolver)

    def on_add_to_relationship(self, sim, target_sim_info, relationship,
                               from_load):
        if relationship._is_object_rel:
            return
        target_sim = target_sim_info.get_sim_instance()
        self.add_buffs_for_bit_add(sim, relationship, from_load)
        if target_sim is not None and self.directionality == RelationshipDirection.BIDIRECTIONAL:
            self.add_buffs_for_bit_add(target_sim, relationship, from_load)
        if not from_load:
            self._apply_bit_added_loot(sim.sim_info, target_sim_info)
            if self.directionality == RelationshipDirection.BIDIRECTIONAL:
                self._apply_bit_added_loot(target_sim_info, sim.sim_info)

    def on_remove_from_relationship(self, sim, target_sim_info):
        target_sim = target_sim_info.get_sim_instance()
        if self._buff_handles is not None:
            for (sim_id, buff_handle) in self._buff_handles:
                if sim.sim_id == sim_id:
                    sim.remove_buff(buff_handle)
                elif target_sim is not None:
                    target_sim.remove_buff(buff_handle)
            self._buff_handles = None

    def add_appropriateness_buffs(self, sim_info):
        if not self._appropriate_buffs_handles:
            if self.buffs_to_add_if_on_active_lot:
                self._appropriate_buffs_handles = []
                for buff in self.buffs_to_add_if_on_active_lot:
                    handle = sim_info.add_buff(buff.buff_type,
                                               buff_reason=buff.buff_reason)
                    self._appropriate_buffs_handles.append(handle)

    def remove_appropriateness_buffs(self, sim_info):
        if self._appropriate_buffs_handles is not None:
            for buff in self._appropriate_buffs_handles:
                sim_info.remove_buff(buff)
            self._appropriate_buffs_handles = None

    def add_conditional_removal_listener(self, listener):
        if self._conditional_removal_listener is not None:
            logger.error(
                'Attempting to add a conditional removal listener when one already exists; old one will be overwritten.',
                owner='jjacobson')
        self._conditional_removal_listener = listener

    def remove_conditional_removal_listener(self):
        listener = self._conditional_removal_listener
        self._conditional_removal_listener = None
        return listener

    def __repr__(self):
        bit_type = type(self)
        return '<({}) Type: {}.{}>'.format(bit_type.__name__,
                                           bit_type.__mro__[1].__module__,
                                           bit_type.__mro__[1].__name__)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.historical_bits is not None:
            for bit in cls.historical_bits:
                pass

    @classmethod
    def commodity_flags(cls):
        if cls._cached_commodity_flags is None:
            commodity_flags = set()
            for super_affordance in cls.get_provided_super_affordances_gen():
                commodity_flags.update(super_affordance.commodity_flags)
            cls._cached_commodity_flags = frozenset(commodity_flags)
        return cls._cached_commodity_flags

    def show_bit_added_dialog(self, owner, sim, target_sim_info):
        dialog = self.bit_added_notification.notification(
            owner, DoubleSimResolver(sim, target_sim_info))
        dialog.show_dialog(additional_tokens=(sim, target_sim_info))

    def show_bit_removed_dialog(self, sim, target_sim_info):
        dialog = self.bit_removed_notification(
            sim, DoubleSimResolver(sim, target_sim_info))
        dialog.show_dialog(additional_tokens=(sim, target_sim_info))

    @classmethod
    def matches_bit(cls, bit_type):
        return cls is bit_type
Esempio n. 13
0
class SituationGoal(SituationGoalDisplayMixin,
                    metaclass=HashedTunedInstanceMetaclass,
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SITUATION_GOAL)):
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {
        '_pre_tests':
        TunableSituationGoalPreTestSet(
            description=
            '\n            A set of tests on the player sim and environment that all must\n            pass for the goal to be given to the player. e.g. Player Sim\n            has cooking skill level 7.\n            ',
            tuning_group=GroupNames.TESTS),
        '_post_tests':
        TunableSituationGoalPostTestSet(
            description=
            '\n            A set of tests that must all pass when the player satisfies the\n            goal_test for the goal to be consider completed. e.g. Player\n            has Drunk Buff when Kissing another sim at Night.\n            ',
            tuning_group=GroupNames.TESTS),
        '_cancel_on_travel':
        Tunable(
            description=
            '\n            If set, this situation goal will cancel (technically, complete\n            with score overridden to 0 so that situation score is not\n            progressed) if situation changes zone.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.TESTS),
        '_environment_pre_tests':
        TunableSituationGoalEnvironmentPreTestSet(
            description=
            '\n            A set of sim independent pre tests.\n            e.g. There are five desks.\n            ',
            tuning_group=GroupNames.TESTS),
        'role_tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n            This goal will only be given to Sims in SituationJobs or Role\n            States marked with one of these tags.\n            '
        ),
        '_cooldown':
        TunableSimMinute(
            description=
            '\n            The cooldown of this situation goal.  Goals that have been\n            completed will not be chosen again for the amount of time that\n            is tuned.\n            ',
            default=600,
            minimum=0),
        '_iterations':
        Tunable(
            description=
            '\n             Number of times the player must perform the action to complete the goal\n             ',
            tunable_type=int,
            default=1),
        '_score':
        Tunable(
            description=
            '\n            The number of points received for completing the goal.\n            ',
            tunable_type=int,
            default=10),
        'score_on_iteration_complete':
        OptionalTunable(
            description=
            '\n            If enabled then we will add an amount of score to the situation\n            with every iteration of the situation goal completing.\n            ',
            tunable=Tunable(
                description=
                '\n                An amount of score that should be applied when an iteration\n                completes.\n                ',
                tunable_type=int,
                default=10)),
        '_pre_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is started.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        '_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'noncancelable':
        Tunable(
            description=
            '\n            Checking this box will prevent the player from canceling this goal in the whim system.',
            tunable_type=bool,
            default=False),
        'time_limit':
        Tunable(
            description=
            '\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.',
            tunable_type=int,
            default=0),
        'penalty_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'goal_awarded_notification':
        OptionalTunable(
            description=
            '\n            If enabled, this goal will have a notification associated with it.\n            It is up to whatever system awards the goal (e.g. the Whim system)\n            to display the notification when necessary.\n            ',
            tunable=TunableUiDialogNotificationSnippet()),
        'goal_completion_notification':
        OptionalTunable(tunable=UiDialogNotification.TunableFactory(
            description=
            '\n                A TNS that will fire when this situation goal is completed.\n                '
        )),
        'goal_completion_notification_and_modal_target':
        OptionalTunable(
            description=
            '\n            If enabled then we will use the tuned situation job to pick a\n            random sim in the owning situation with that job to be the target\n            sim of the notification and modal dialog.\n            ',
            tunable=TunableReference(
                description=
                '\n                The situation job that will be used to find a sim in the owning\n                situation to be the target sim.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.SITUATION_JOB))),
        'audio_sting_on_complete':
        TunableResourceKey(
            description=
            '\n            The sound to play when this goal is completed.\n            ',
            resource_types=(sims4.resources.Types.PROPX, ),
            default=None,
            allow_none=True,
            tuning_group=GroupNames.AUDIO),
        'goal_completion_modal_dialog':
        OptionalTunable(tunable=UiDialogOk.TunableFactory(
            description=
            '\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '
        )),
        'visible_minor_goal':
        Tunable(
            description=
            '\n            Whether or not this goal should be displayed in the minor goals\n            list if this goal is for a player facing situation.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.UI),
        'display_type':
        TunableEnumEntry(
            description=
            '\n            How this goal is presented in user-facing situations.\n            ',
            tunable_type=SituationGoalDisplayType,
            default=SituationGoalDisplayType.NORMAL,
            tuning_group=GroupNames.UI)
    }

    @classmethod
    def can_be_given_as_goal(cls, actor, situation, **kwargs):
        if actor is not None:
            resolver = event_testing.resolver.DataResolver(
                actor.sim_info, None)
            result = cls._pre_tests.run_tests(resolver)
            if not result:
                return result
        else:
            resolver = GlobalResolver()
        environment_test_result = cls._environment_pre_tests.run_tests(
            resolver)
        if not environment_test_result:
            return environment_test_result
        return TestResult.TRUE

    def __init__(self,
                 sim_info=None,
                 situation=None,
                 goal_id=0,
                 count=0,
                 locked=False,
                 completed_time=None,
                 secondary_sim_info=None,
                 **kwargs):
        self._sim_info = sim_info
        self._secondary_sim_info = secondary_sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = completed_time
        self._count = count
        self._locked = locked
        self._score_override = None
        self._goal_status_override = None
        self._setup = False

    def setup(self):
        self._setup = True

    def destroy(self):
        self.decommision()
        self._sim_info = None
        self._situation = None

    def decommision(self):
        if self._setup:
            self._decommision()

    def _decommision(self):
        self._on_goal_completed_callbacks.clear()

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        target_sim_info = self.get_required_target_sim_info()
        target_id = 0 if target_sim_info is None else target_sim_info.sim_id
        secondary_target_id = 0 if self._secondary_sim_info is None else self._secondary_sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(
            type(self), actor_id, target_id, secondary_target_id, self._count,
            self._locked, self._completed_time)
        return seedling

    def register_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.append(listener)

    def unregister_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.remove(listener)

    def get_gsi_name(self):
        if self._iterations <= 1:
            return self.__class__.__name__
        return '{} {}/{}'.format(self.__class__.__name__, self._count,
                                 self._iterations)

    def on_goal_offered(self):
        if self._situation is None:
            return
        for sim in self._situation.all_sims_in_situation_gen():
            resolver = sim.get_resolver()
            for loots in self._pre_goal_loot_list:
                for loot in loots.goal_loot_actions:
                    loot.apply_to_resolver(resolver)

    def _display_goal_completed_dialogs(self):
        actor_sim_info = services.active_sim_info()
        target_sim_info = None
        if self.goal_completion_notification_and_modal_target is not None:
            possible_sims = list(
                self._situation.all_sims_in_job_gen(
                    self.goal_completion_notification_and_modal_target))
            if possible_sims:
                target_sim_info = random.choice(possible_sims)
            if target_sim_info is None:
                return
        resolver = DoubleSimResolver(actor_sim_info, target_sim_info)
        if self.goal_completion_notification is not None:
            notification = self.goal_completion_notification(actor_sim_info,
                                                             resolver=resolver)
            notification.show_dialog()
        if self.goal_completion_modal_dialog is not None:
            dialog = self.goal_completion_modal_dialog(actor_sim_info,
                                                       resolver=resolver)
            dialog.show_dialog()

    def _on_goal_completed(self, start_cooldown=True):
        if start_cooldown:
            self._completed_time = services.time_service().sim_now
        loot_sims = (self._sim_info, ) if self._situation is None else tuple(
            self._situation.all_sims_in_situation_gen())
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in loot_sims:
                    loot.apply_to_resolver(sim.get_resolver())
        self._display_goal_completed_dialogs()
        with situations.situation_manager.DelayedSituationDestruction():
            self._on_goal_completed_callbacks(self, True)

    def _on_iteration_completed(self):
        self._on_goal_completed_callbacks(self, False)

    def force_complete(self,
                       target_sim=None,
                       score_override=None,
                       start_cooldown=True):
        self._score_override = score_override
        self._count = self._iterations
        self._on_goal_completed(start_cooldown=start_cooldown)

    def _valid_event_sim_of_interest(self, sim_info):
        return self._sim_info is None or self._sim_info is sim_info

    def handle_event(self, sim_info, event, resolver):
        if not self._valid_event_sim_of_interest(sim_info):
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            self._count += 1
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

    def _run_goal_completion_tests(self, sim_info, event, resolver):
        return self._post_tests.run_tests(resolver)

    def should_autocomplete_on_load(self, previous_zone_id):
        if self._cancel_on_travel:
            zone_id = services.current_zone_id()
            if previous_zone_id != zone_id:
                return True
        return False

    def get_actual_target_sim_info(self):
        pass

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

    def get_required_target_sim_info(self):
        pass

    def get_secondary_sim_info(self):
        return self._secondary_sim_info

    @property
    def created_time(self):
        pass

    @property
    def completed_time(self):
        return self._completed_time

    def is_on_cooldown(self):
        if self._completed_time is None:
            return False
        time_since_last_completion = services.time_service(
        ).sim_now - self._completed_time
        return time_since_last_completion < interval_in_sim_minutes(
            self._cooldown)

    def get_localization_tokens(self):
        target_sim_info = self.get_required_target_sim_info()
        return (self._numerical_token, self._sim_info, target_sim_info,
                self._secondary_sim_info)

    def get_display_name(self):
        display_name = self.display_name
        if display_name is not None:
            return display_name(*self.get_localization_tokens())

    def get_display_tooltip(self):
        display_tooltip = self.display_tooltip
        if display_tooltip is not None:
            return display_tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        if self._score_override is not None:
            return self._score_override
        return self._score

    @property
    def goal_status_override(self):
        return self._goal_status_override

    @property
    def completed_iterations(self):
        return self._count

    @property
    def max_iterations(self):
        return self._iterations

    @property
    def _numerical_token(self):
        return self.max_iterations

    @property
    def locked(self):
        return self._locked

    def toggle_locked_status(self):
        self._locked = not self._locked

    def validate_completion(self):
        if self._completed_time is not None:
            return
        if self.completed_iterations < self.max_iterations:
            return
        self.force_complete()

    def show_goal_awarded_notification(self):
        if self.goal_awarded_notification is None:
            return
        icon_override = IconInfoData(icon_resource=self.display_icon)
        secondary_icon_override = IconInfoData(obj_instance=self._sim_info)
        notification = self.goal_awarded_notification(self._sim_info)
        notification.show_dialog(
            additional_tokens=self.get_localization_tokens(),
            icon_override=icon_override,
            secondary_icon_override=secondary_icon_override)
Esempio n. 14
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')
Esempio n. 15
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. 16
0
class GameRules(HasTunableReference,
                metaclass=TunedInstanceMetaclass,
                manager=services.get_instance_manager(
                    sims4.resources.Types.GAME_RULESET)):
    ENDING_CONDITION_SCORE = 0
    ENDING_CONDITION_ROUND = 1
    INSTANCE_TUNABLES = {
        'game_name':
        TunableLocalizedStringFactory(
            description='\n            Name of the game.\n            ',
            default=1860708663),
        'team_strategy':
        TunableVariant(
            description=
            '\n            Define how Sims are distributed across teams.\n            ',
            auto_balanced=GameTeamAutoBalanced.TunableFactory(),
            part_driven=GameTeamPartDriven.TunableFactory(),
            default='auto_balanced'),
        'teams_per_game':
        TunableInterval(
            description=
            '\n            An interval specifying the number of teams allowed per game.\n            \n            Joining Sims are put on a new team if the maximum number of teams\n            has not yet been met, otherwise they are put into the team with the\n            fewest number of players.\n            ',
            tunable_type=int,
            default_lower=2,
            default_upper=2,
            minimum=1),
        'players_per_game':
        TunableInterval(
            description=
            '\n            An interval specifying the number of players allowed per game.\n            \n            If the maximum number of players has not been met, Sims can\n            continue to join a game.  Joining Sims are put on a new team if the\n            maximum number of teams as specified in the "teams_per_game"\n            tunable has not yet been met, otherwise they are put into the team\n            with the fewest number of players.\n            ',
            tunable_type=int,
            default_lower=2,
            default_upper=2,
            minimum=1),
        'players_per_turn':
        TunableRange(
            description=
            '\n            An integer specifying number of players from the active team who\n            take their turn at one time.\n            ',
            tunable_type=int,
            default=1,
            minimum=1),
        'initial_state':
        ObjectStateValue.TunableReference(
            description=
            "\n            The game's starting object state.\n            ",
            allow_none=True),
        'score_info':
        TunableTuple(
            description=
            "\n            Tunables that affect the game's score.\n            ",
            ending_condition=TunableVariant(
                description=
                '\n                The condition under which the game ends.\n                ',
                score_based=TunableTuple(
                    description=
                    '\n                    A game that ends when one of the teams wins by reaching a \n                    certain score first\n                    ',
                    locked_args={'end_condition': ENDING_CONDITION_SCORE},
                    winning_score=Tunable(
                        description=
                        '\n                        Score required to win.\n                        ',
                        tunable_type=int,
                        default=100)),
                round_based=TunableTuple(
                    description=
                    '\n                    A game that ends after a certain number of rounds.  The Team\n                    with the highest score at that point wins.\n                    ',
                    locked_args={'end_condition': ENDING_CONDITION_ROUND},
                    rounds=Tunable(
                        description=
                        '\n                        Length of game (in rounds).\n                        ',
                        tunable_type=int,
                        default=3)),
                default='score_based'),
            score_increase=TunableInterval(
                description=
                '\n                An interval specifying the minimum and maximum score increases\n                possible in one turn. A random value in this interval will be\n                generated each time score loot is given.\n                ',
                tunable_type=int,
                default_lower=35,
                default_upper=50,
                minimum=0),
            allow_scoring_for_non_active_teams=Tunable(
                description=
                '\n                If checked, any Sim may score, even if their team is not\n                considered active.\n                ',
                tunable_type=bool,
                default=False),
            skill_level_bonus=Tunable(
                description=
                "\n                A bonus number of points based on the Sim's skill level in the\n                relevant_skill tunable that will be added to score_increase.\n                \n                ex: If this value is 2 and the Sim receiving score has a\n                relevant skill level of 4, they will receive 8 (2 * 4) extra\n                points.\n                ",
                tunable_type=float,
                default=2),
            relevant_skill=Skill.TunableReference(
                description=
                "\n                The skill relevant to this game.  Each Sim's proficiency in\n                this skill will effect the score increase they get.\n                ",
                allow_none=True),
            use_effective_skill_level=Tunable(
                description=
                '\n                If checked, we will use the effective skill level rather than\n                the actual skill level of the relevant_skill tunable.\n                ',
                tunable_type=bool,
                default=True),
            progress_stat=Statistic.TunableReference(
                description=
                '\n                The statistic that advances the progress state of this game.\n                ',
                allow_none=True),
            persist_high_score=Tunable(
                description=
                '\n                If checked, the high score and the team Sim ids will be\n                saved onto the game component.\n                ',
                tunable_type=bool,
                default=False)),
        'clear_score_on_player_join':
        Tunable(
            description=
            '\n            Tunable that, when checked, will clear the game score when a player joins.\n            \n            This essentially resets the game.\n            ',
            tunable_type=bool,
            default=False),
        'alternate_target_object':
        OptionalTunable(
            description=
            '\n            Tunable that, when enabled, means the game should create an alternate object\n            in the specified slot on setup that will be modified as the game goes on\n            and destroyed when the game ends.\n            ',
            tunable=TunableTuple(
                target_game_object=TunableReference(
                    description=
                    '\n                    The definition of the object that will be created/destroyed/altered\n                    by the game.\n                    ',
                    manager=services.definition_manager()),
                parent_slot=TunableVariant(
                    description=
                    '\n                    The slot on the parent object where the target_game_object object should go. This\n                    may be either the exact name of a bone on the parent object or a\n                    slot type, in which case the first empty slot of the specified type\n                    in which the child object fits will be used.\n                    ',
                    by_name=Tunable(
                        description=
                        '\n                        The exact name of a slot on the parent object in which the target\n                        game object should go.  \n                        ',
                        tunable_type=str,
                        default='_ctnm_'),
                    by_reference=TunableReference(
                        description=
                        '\n                        A particular slot type in which the target game object should go.  The\n                        first empty slot of this type found on the parent will be used.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.SLOT_TYPE))),
                destroy_at_end=Tunable(
                    description=
                    '\n                    If True, the alternate target object will get destroyed at the end of the game.\n                    ',
                    tunable_type=bool,
                    default=True))),
        'game_over_notification':
        OptionalTunable(
            description=
            "\n            If enabled, when any Sim involved in the game is a player-controlled\n            Sim, display a notification when the game is over.\n            \n            NOTE: As of now, this only triggers when there are *exactly* two\n            teams. To support more teams, we'll need to extend the possible\n            string permutation.\n            ",
            tunable=TunableTuple(
                one_v_one=TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when the game is 1v1.\n                    \n                     * Token 0 is the object the game is being played on\n                     * Token 1 is the winner\n                     * Token 2 is the loser\n                     * Token 3 is the winner's score\n                     * Token 4 is the loser's score\n                    "
                ),
                one_v_many_winner
                =TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when the game is 1 v many, and the\n                    single Sim is the winner.\n                    \n                    * Token 0 is the object the game is being played on\n                    * Token 1 is the winner\n                    * Token 2 is a list of losers (Alice, Bob, and Carol)\n                    * Token 3 is the winner's score\n                    * Token 4 is the loser's score\n                    "
                ),
                one_v_many_loser=TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when the game is 1 v many, and the\n                    single Sim is the loser.\n                    \n                    * Token 0 is the object the game is being played on\n                    * Token 1 is a list of winners (Alice, Bob, and Carol)\n                    * Token 2 is the loser\n                    * Token 3 is the winner's score\n                    * Token 4 is the loser's score\n                    "
                ),
                many_v_many=TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when the game is many v many.\n                    \n                    * Token 0 is the object the game is being played on\n                    * Token 1 is a list of winners (Alice and Bob)\n                    * Token 2 is a list of losers (Carol, Dan, and Erin)\n                    * Token 3 is the winner's score\n                    * Token 4 is the loser's score\n                    "
                ))),
        'game_over_winner_only_notification':
        OptionalTunable(
            description=
            '\n            If enabled, when any Sim involved in the game is a player-controlled\n            Sim, display a notification when the game is over.\n            \n            NOTE: This will show only the winners of the game with the highest \n            score. The winners can be more than one team if they have same \n            score.\n            ',
            tunable=TunableTuple(
                play_alone=TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when Sim play alone.\n                    \n                    * Token 0 is the object the game is being played on\n                    * Token 1 is the Sim's name\n                    * Token 2 is the Sim's score\n                    "
                ),
                winner=TunableUiDialogNotificationSnippet(
                    description=
                    "\n                    The notification to show when the game has 1 team winner.\n                    \n                    * Token 0 is the object the game is being played on\n                    * Token 1 is the winner\n                    * Token 2 is the winner's score\n                    "
                ))),
        'additional_game_behavior':
        OptionalTunable(
            description=
            '\n            If enabled additional behavior will be run for this type of game\n            on multiple phases like creating destroying additional objects on \n            setup of end phases.\n            ',
            tunable=TunableVariant(
                description=
                "\n                Variant of type of games that will add very specific behavior\n                to the game component.\n                e.g. Card battle behavior will create cards and destroy them\n                depending on each actor's inventory.\n                ",
                card_battle=CardBattleBehavior.TunableFactory(),
                create_object=CreateObjectBehavior.TunableFactory(),
                default='card_battle'))
    }

    def __init__(self, game_component, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._game_component = game_component
        self.additional_game_behavior = self.additional_game_behavior(
        ) if self.additional_game_behavior is not None else None

    def add_player(self, sim):
        self.team_strategy.add_player(self._game_component, sim)

    @classmethod
    def can_be_on_same_team(cls, target_a, target_b):
        return cls.team_strategy.can_be_on_same_team(target_a, target_b)

    @classmethod
    def team_determines_part(cls):
        return cls.team_strategy.team_determines_part()

    @classmethod
    def can_be_on_opposing_team(cls, target_a, target_b):
        return cls.team_strategy.can_be_on_opposing_team(target_a, target_b)

    def remove_player(self, sim):
        self.team_strategy.remove_player(self._game_component, sim)
Esempio n. 17
0
class SituationJob(HasDependentTunableReference,
                   metaclass=HashedTunedInstanceMetaclass,
                   manager=services.situation_job_manager()):
    __qualname__ = 'SituationJob'
    CHANGE_OUTFIT_INTERACTION = interactions.base.super_interaction.SuperInteraction.TunableReference(
        description=
        '\n        A reference that should be tuned to an interaction that will just set\n        sim to their default outfit.\n        '
    )
    INSTANCE_TUNABLES = {
        'display_name':
        TunableLocalizedString(
            description=
            '\n                Localized name of this job. This name is displayed in the situation\n                creation UI where the player is making selection of sims that belong\n                to a specific job. E.g. "Guest", "Bride or Groom", "Bartender".\n                \n                Whenever you add a display name, evaluate whether your design \n                needs or calls out for a tooltip_name.\n                ',
            tuning_group=GroupNames.UI),
        'tooltip_name':
        TunableLocalizedString(
            description=
            '\n                Localized name of this job that is displayed when the player hovers\n                on the sim while the situation is in progress. If this field is absent, \n                there will be no tooltip on the sim.\n                \n                This helps distinguish the cases where we want to display "Bride or Groom" \n                in the situation creation UI but only "Bride" or "Groom" on the \n                sim\'s tooltip when the player is playing with the situation. \n                ',
            tuning_group=GroupNames.UI),
        'icon':
        TunableResourceKey(
            description=
            '\n                Icon to be displayed for the job of the Sim\n                ',
            default=None,
            needs_tuning=True,
            resource_types=sims4.resources.CompoundTypes.IMAGE,
            tuning_group=GroupNames.UI),
        'job_description':
        TunableLocalizedString(
            description=
            '\n                Localized description of this job\n                ',
            tuning_group=GroupNames.UI),
        'tests':
        event_testing.tests.TunableTestSet(),
        'sim_auto_invite':
        TunableInterval(
            description=
            "\n                On situation start it will select a random number of sims in this interval.\n                It will automatically add npcs to the situation so that it has at least\n                that many sims in this job including those the player\n                invites/hires. If the player invites/hires more than the auto\n                invite number, no npcs will be automatically added.\n                \n                Auto invite sims are considered to be invited, so they will be\n                spawned for invite only situations too. For player initiated\n                situations you probably want to set this 0. It is really meant\n                for commercial venues.\n                \n                You can use Churn tuning on this job if you want the number of\n                sims to vary over time. Churn tuning will override this one.\n                \n                For example, an ambient bar situation would have a high auto\n                invite number for the customer job because we want many sims in\n                the bar but the player doesn't invite or hire anybody for an\n                ambient situation.\n                \n                A date would have 0 for this for all jobs because the situation\n                would never spawn anybody to fill jobs, the player would have\n                to invite them all.\n                ",
            tunable_type=int,
            default_lower=0,
            default_upper=0,
            minimum=0),
        'sim_auto_invite_allow_instanced_sim':
        Tunable(
            description=
            '\n                If checked will allow instanced sims to be assigned this job\n                to fulfill auto invite spots instead of forcing the spawning\n                of new sims.\n                \n                NOTE: YOU PROBABLY WANT TO LEAVE THIS AS UNCHECKED.  PLEASE\n                CONSULT A GPE IF YOU PLAN ON TUNING IT.\n                ',
            tunable_type=bool,
            default=False),
        'sim_count':
        TunableInterval(
            description=
            '\n                The number of Sims the player is allowed to invite or hire for\n                this job.  The lower bound is the required number of sims, the\n                upper bound is the maximum.\n                \n                This only affects what the player can do in the Plan an Event UI.\n                It has no affect while the situation is running.\n                ',
            tunable_type=int,
            default_lower=1,
            default_upper=1,
            minimum=0),
        'churn':
        OptionalTunable(
            description=
            'If enabled, produces churn or turnover\n                in the sims holding this job. Periodically sims in the job will leave\n                the lot and other sims will come to fill the job. \n                \n                When a situation is first started it will automatically invite a\n                number of sims appropriate for the time of day. This supercedes\n                sim_auto_invite.\n                \n                This is primarily for commercial venue customers.\n                This is NOT compatible with Sim Shifts.\n                ',
            tunable=SituationJobChurn.TunableFactory(),
            display_name='Sim Churn'),
        'sim_shifts':
        OptionalTunable(
            description=
            'If enabled, creates shifts of\n                sims who replace the sims currently in the job.\n                \n                When a situation is first started it will automatically invite a\n                number of sims appropriate for the time of day. This supercedes\n                sim_auto_invite.\n                \n                This is primarily intended for commercial venue employees.\n                This is NOT compatible with Sim Churn.\n                ',
            tunable=SituationJobShifts.TunableFactory()),
        'goal_scoring':
        Tunable(
            description=
            '\n                The score for completing a goal\n                ',
            tunable_type=int,
            default=1,
            tuning_group=GroupNames.SCORING),
        'interaction_scoring':
        TunableList(
            description=
            '\n                Test for interactions run. Each test can have a separate score.\n                ',
            tunable=TunableTuple(
                description=
                '\n                    Each affordance that satisfies the test will receive the\n                    same score.\n                    ',
                score=Tunable(
                    description=
                    '\n                        Score for passing the test.\n                        ',
                    tunable_type=int,
                    default=1),
                affordance_list=event_testing.tests_with_data.
                TunableParticipantRanInteractionTest(
                    locked_args={
                        'participant': ParticipantType.Actor,
                        'tooltip': None,
                        'running_time': None
                    })),
            tuning_group=GroupNames.SCORING),
        'crafted_object_scoring':
        TunableList(
            description=
            '\n                Test for objects crafted. Each test can have a separate score.\n                ',
            tunable=TunableTuple(
                description=
                '\n                    Test for objects crafted. Each test can have a separate\n                    score.\n                    ',
                score=Tunable(
                    description=
                    '\n                        Score for passing the test.\n                        ',
                    tunable_type=int,
                    default=1),
                object_list=event_testing.test_variants.TunableCraftedItemTest(
                    description=
                    '\n                        A test to see if the crafted item should give score.\n                        ',
                    locked_args={'tooltip': None})),
            tuning_group=GroupNames.SCORING),
        'rewards':
        TunableMapping(
            description=
            '\n                Rewards given to the sim in this job when situation reaches specific medals.\n                ',
            key_type=TunableEnumEntry(
                SituationMedal,
                SituationMedal.TIN,
                description=
                '\n                    Medal to achieve to get the corresponding benefits.\n                    '
            ),
            value_type=SituationJobReward.TunableFactory(
                description=
                '\n                    Reward and LootAction benefits for accomplishing the medal.\n                    '
            ),
            key_name='medal',
            value_name='benefit',
            tuning_group=GroupNames.SCORING),
        'filter':
        TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.SIM_FILTER),
                         needs_tuning=True,
                         class_restrictions=TunableSimFilter),
        'tags':
        TunableSet(
            description=
            '\n                Designer tagging for making the game more fun.\n                ',
            tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)),
        'job_uniform':
        OptionalTunable(
            description=
            '\n                If enabled, when a Sim is assigned this situation job, that Sim\n                will switch into their outfit based on the Outfit Category.\n                \n                If the Outfit Category is SITUATION, then an outfit will be\n                generated based on the passed in tags and the Sim will switch\n                into that outfit.\n                ',
            tunable=TunableTuple(
                description='\n                    ',
                outfit_change_reason=TunableEnumEntry(
                    description=
                    '\n                        An enum that represents a reason for outfit change for\n                        the outfit system.\n                        \n                        An outfit change reason is really a series of tests\n                        that are run to determine which outfit category that\n                        we want to switch into.\n                        \n                        In order to do this, go to the tuning file\n                        sims.sim_outfits.\n                        \n                        Add a new OutfitChangeReason enum entry for your change\n                        reason.\n                        \n                        Then go into\n                        ClothingChangeTunables->Clothing Reasons To Outfits\n                        and add a new entry to the map.\n                        \n                        Set this entry to your new enum entry.\n                        \n                        Then you can add new elements to the list of tests and\n                        outfit categories that you want it to change the sim\n                        into.\n                        ',
                    tunable_type=OutfitChangeReason,
                    default=OutfitChangeReason.Invalid),
                outfit_change_priority=TunableEnumEntry(
                    description=
                    '\n                        The outfit change priority.  Higher priority outfit\n                        changes will override lower priority outfit changes.\n                        ',
                    tunable_type=DefaultOutfitPriority,
                    default=DefaultOutfitPriority.NoPriority),
                playable_sims_change_outfits=Tunable(
                    description=
                    '\n                        If checked, Playable Sims will change outfit when the job is set for the Sim. This\n                        should be checked on things like user facing events,\n                        but not Venue Background Event Jobs.\n                        ',
                    tunable_type=bool,
                    default=True),
                situation_outfit_generation_tags=OptionalTunable(
                    description=
                    "\n                        If enabled, the situation will use the outfit tags\n                        specified to generate an outfit for the sim's\n                        SITUATION outfit category.  If generating an outfit\n                        make sure to set outfit change reason to something that\n                        will put the sim into the SITUATION outfit category or\n                        you will not have the results that you expect.\n                        ",
                    tunable=TunableSet(
                        description=
                        '\n                            Only one of these sets is picked randomly to select the\n                            outfit tags within this set.\n                            E.g. If you want to host a costume party where the guests show\n                            up in either octopus costume or a shark costume, we would have\n                            two sets of tuning that can specify exclusive tags for the \n                            specific costumes. Thus we avoid accidentally generating a \n                            sharktopus costume.\n                            \n                            If you want your guests to always show up in sharktopus costumes \n                            then tune only one set of tags that enlist all the outfit tags\n                            that are associated with either shark or octopus.\n                            ',
                        tunable=TunableSet(
                            description=
                            "\n                                Tags that will be used by CAS to generate an outfit\n                                within the sim's SITUATION outfit category.\n                                ",
                            tunable=TunableEnumWithFilter(
                                tunable_type=Tag,
                                filter_prefixes=['uniform', 'outfitcategory'],
                                default=Tag.INVALID))))),
            disabled_name='no_uniform',
            enabled_name='uniform_specified'),
        'can_be_hired':
        Tunable(description=
                '\n                This job can be hired.\n                ',
                tunable_type=bool,
                default=True),
        'hire_cost':
        Tunable(
            description=
            '\n                The cost to hire a Sim for this job in Simoleons.\n                ',
            tunable_type=int,
            default=0),
        'game_breaker':
        Tunable(
            description=
            '\n                If True then this job must be filled by a sim\n                or the game will be broken. This is for the grim reaper and\n                the social worker.\n                ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES,
            tuning_filter=FilterTag.EXPERT_MODE),
        'elevated_importance':
        Tunable(
            description=
            '\n                If True, then filling this job with a Sim will be done before\n                filling similar jobs in this situation. This will matter when\n                starting a situation on another lot, when inviting a large number\n                of Sims, visiting commercial venues, or when at the cap on NPCs.\n                \n                Examples:\n                Wedding Situation: the Bethrothed Sims should be spawned before any guests.\n                Birthday Party: the Sims whose birthday it is should be spawned first.\n                Bar Venue: the Bartender should be spawned before the barflies.\n                \n                ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES,
            tuning_filter=FilterTag.EXPERT_MODE),
        'no_show_action':
        TunableEnumEntry(
            situations.situation_types.JobHolderNoShowAction,
            default=situations.situation_types.JobHolderNoShowAction.
            DO_NOTHING,
            description=
            "\n                                The action to take if no sim shows up to fill this job.\n                                \n                                Examples: \n                                If your usual maid doesn't show up, you want another one (REPLACE_THEM).\n                                If one of your party guests doesn't show up, you don't care (DO_NOTHING)\n                                If the President of the United States doesn't show up for the inauguration, you are hosed (END_SITUATION)\n                                ",
            tuning_group=GroupNames.SPECIAL_CASES),
        'died_or_left_action':
        TunableEnumEntry(
            situations.situation_types.JobHolderDiedOrLeftAction,
            default=situations.situation_types.JobHolderDiedOrLeftAction.
            DO_NOTHING,
            description=
            "\n                                    The action to take if a sim in this job dies or leaves the lot.\n                                    \n                                    Examples: \n                                    If the bartender leaves the ambient bar situation, you need a new one (REPLACE_THEM)\n                                    If your creepy uncle leaves the wedding, you don't care (DO_NOTHING)\n                                    If your maid dies cleaning the iron maiden, you are out of luck for today (END_SITUATION).\n                                    \n                                    NB: Do not use REPLACE_THEM if you are using Sim Churn for this job.\n                                    ",
            tuning_group=GroupNames.SPECIAL_CASES),
        'sim_spawner_tags':
        TunableList(
            description=
            '\n            A list of tags that represent where to spawn Sims for this Job when they come onto the lot.\n            NOTE: Tags will be searched in order of tuning. Tag [0] has priority over Tag [1] and so on.\n            ',
            tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)),
        'sim_spawn_action':
        TunableSpawnActionVariant(
            description=
            '\n            Define the methods to show the Sim after spawning on the lot.\n            '
        ),
        'sim_spawner_leave_option':
        TunableEnumEntry(
            description=
            '\n            The method for selecting spawn points for sims that are\n            leaving the lot. \n            \n            TUNED CONSTRAINT TAGS come from the SpawnPointConstraint.\n            SAVED TAGS are the same tags that were used to spawn the sim. \n            SAME vs DIFFERENT vs ANY resolves how to use the spawn point\n            that was saved when the sim entered the lot.\n            ',
            tunable_type=SpawnPointOption,
            default=SpawnPointOption.SPAWN_SAME_POINT),
        'emotional_setup':
        TunableList(
            description=
            '\n                Apply the WeightedSingleSimLootActions on the sim that is assigned this job. These are applied\n                only on NPC sims since the tuning is forcing changes to emotions.\n                \n                E.g. an angry mob at the bar, flirty guests at a wedding party.\n                ',
            tunable=TunableTuple(
                single_sim_loot_actions=WeightedSingleSimLootActions.
                TunableReference(),
                weight=Tunable(
                    int, 1, description='Accompanying weight of the loot.')),
            tuning_group=GroupNames.ON_CREATION),
        'commodities':
        TunableList(
            description=
            '\n                Update the commodities on the sim that is assigned this job. These are applied only on\n                NPC sims since the tuning is forcing changes to statistics that have player facing effects.\n             \n                E.g. The students arrive at the lecture hall with the bored and sleepy commodities.\n                ',
            tunable=TunableStatisticChange(
                locked_args={
                    'subject': ParticipantType.Actor,
                    'advertise': False,
                    'chance': 1,
                    'tests': None
                }),
            tuning_group=GroupNames.ON_CREATION),
        'requirement_text':
        TunableLocalizedString(
            description=
            '\n                A string that will be displayed in the sim picker for this\n                job in the situation window.\n                '
        ),
        'goodbye_notification':
        TunableVariant(
            description=
            '\n                The "goodbye" notification that will be set on Sims with this\n                situation job. This notification will be displayed when the\n                Sim leaves the lot (unless it gets overridden later).\n                Examples: the visitor job sets the "goodbye" notification to\n                something so the player knows when visitors leave; the party\n                guest roles use "No Notification", because we don\'t want 20-odd\n                notifications when a party ends; the leave lot jobs use "Use\n                Previous Notification" because we want leaving Sims to display\n                whatever notification was set earlier.\n                ',
            notification=TunableUiDialogNotificationSnippet(),
            locked_args={
                'no_notification':
                None,
                'never_use_notification_no_matter_what':
                SetGoodbyeNotificationElement.
                NEVER_USE_NOTIFICATION_NO_MATTER_WHAT,
                'use_previous_notification':
                DEFAULT
            },
            default='no_notification'),
        'additional_filter_for_user_selection':
        TunableReference(
            description=
            '\n                An additional filter that will run for the situation job if\n                there should be specific additional requirements for selecting\n                specific sims for the role rather than hiring them.\n                ',
            manager=services.get_instance_manager(
                sims4.resources.Types.SIM_FILTER),
            needs_tuning=True),
        'recommended_objects':
        TunableList(
            description=
            '\n                A list of objects that are recommended to be on a lot to get\n                the most out of this job\n                ',
            tunable=TunableVenueObject(
                description=
                "\n                        Specify object tag(s) that should be on this lot.\n                        Allows you to group objects, i.e. weight bench,\n                        treadmill, and basketball goals are tagged as\n                        'exercise objects.'\n                        "
            ),
            export_modes=ExportModes.All)
    }

    @classmethod
    def _verify_tuning_callback(cls):
        messages = []
        if cls.died_or_left_action == situations.situation_types.JobHolderDiedOrLeftAction.REPLACE_THEM:
            messages.append('Died Or Left Action == REPLACE_THEM')
        if cls.churn is not None:
            messages.append('Sim Churn')
        if cls.sim_shifts is not None:
            messages.append('Sim Shifts')
        if len(messages) > 1:
            message = ', and '.join(messages)
            logger.error('Situation job :{} must use only one of {}', cls,
                         message)

    @classmethod
    def get_score(cls, event, resolver, **kwargs):
        if event == event_testing.test_variants.TestEvent.InteractionComplete:
            for score_list in cls.interaction_scoring:
                while resolver(score_list.affordance_list):
                    return score_list.score
        elif event == event_testing.test_variants.TestEvent.ItemCrafted:
            for score_list in cls.crafted_object_scoring:
                while resolver(score_list.object_list):
                    return score_list.score
        return 0

    @classmethod
    def get_goal_score(cls):
        return cls.goal_scoring

    @classmethod
    def get_auto_invite(cls):
        if cls.churn is not None:
            interval = cls.churn.get_auto_populate_interval()
        else:
            if cls.sim_shifts is not None:
                return cls.sim_shifts.get_shift_staffing()
            if cls.sim_auto_invite.upper_bound > 0:
                interval = AutoPopulateInterval(
                    min=cls.sim_auto_invite.lower_bound,
                    max=cls.sim_auto_invite.upper_bound)
            else:
                return 0
        auto_invite = random.randrange(interval.min, interval.max + 1)
        return auto_invite

    @classmethod
    def can_sim_be_given_job(cls, sim_id, requesting_sim_info):
        if cls.filter is None:
            return True
        household_id = 0
        if requesting_sim_info is not None:
            household_id = requesting_sim_info.household.id
        return services.sim_filter_service().does_sim_match_filter(
            sim_id,
            sim_filter=cls.filter,
            requesting_sim_info=requesting_sim_info,
            household_id=household_id)
class StaffMemberSituation(StaffedObjectSituationMixin,
                           SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'situation_job':
        SituationJob.TunableReference(
            description=
            '\n            The job that a staff member will be in during the situation.\n            '
        ),
        'actively_working_timeout':
        TunableSimMinute(
            description=
            '\n            The timeout for a staff member in the actively working state.\n            If none of the return_to_actively_working_interactions are run before\n            time expires then the therapist will transition to the bored state.\n            ',
            default=60,
            tuning_group=SituationComplexCommon.TIMEOUT_GROUP),
        'bored_timeout':
        OptionalTunable(
            description=
            "\n            If this is enabled then the bored state will have a timeout. If \n            the timer goes off then the Sim will leave. Leave this disabled if\n            you don't ever want a Sim to leave (e.g. a venue staff person)\n            ",
            tunable=TunableSimMinute(
                description=
                '\n                The timeout for a staff member in the bored state. If none of\n                the return_to_actively_working_interactions are run before the\n                timeout expires then the therapist will transition to the leaving\n                state.\n                ',
                default=60,
                tuning_group=SituationComplexCommon.TIMEOUT_GROUP)),
        'force_sim_to_leave_lot_on_completion':
        Tunable(
            description=
            '\n            If set to True, when a Sim enters the leaving state she will be\n            forced to leave the lot right away.\n            \n            If set to False, when a Sim enters the leaving state she will leave\n            at her earliest convenience.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=SituationComplexCommon.TIMEOUT_GROUP),
        'arrival_notification':
        OptionalTunable(
            description=
            '\n            When enabled, when the Sim arrives on the lot this notification \n            will be displayed to announce their arrival.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification that is displayed whenever a Sim times out while\n                waiting and leaves the lot.\n                '
            ),
            enabled_by_default=True,
            tuning_group=SituationComplexCommon.NOTIFICATION_GROUP),
        'bored_timeout_notification':
        OptionalTunable(
            description=
            '\n            When enabled, when the bored timeout expires and the staff \n            member advances to the leaving state, this notification will be\n            displayed.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                A notification letting the user know that the staff member\n                is done standing around being bored. This likely means that\n                the time has come for the staff member to leave.\n                '
            ),
            enabled_by_default=True,
            tuning_group=SituationComplexCommon.NOTIFICATION_GROUP),
        '_arriving_situation_state':
        _ArrivingState.TunableFactory(
            description=
            '\n            The situation state used for when a Sim is arriving as a staff \n            member.\n            ',
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='01_arriving_situation_state'),
        '_actively_working_situation_state':
        _ActivelyWorkingState.TunableFactory(
            description=
            '\n            The situation state when a staff member is standing \n            professionally around the table and not much else. If they spend\n            too much time in this state without doing any work it will progress\n            to the bored state.\n            ',
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='02_actively_working_situation_state'),
        '_bored_situation_state':
        _BoredState.TunableFactory(
            description=
            '\n            The situation state for the staff member that has been \n            standing idly by for a while without working. If the staff member\n            is in this state too long without working then they will leave.\n            ',
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='03_bored_situation_state'),
        'arrival_interaction':
        OptionalTunable(
            description=
            '\n            The interaction to push on the staff member in this situation when\n            they enter the ArrivingState.\n            ',
            disabled_name='not_required',
            enabled_name='push_interaction',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.INTERACTION)))
    }
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def _states(cls):
        return [
            SituationStateData(1,
                               _ArrivingState,
                               factory=cls._arriving_situation_state),
            SituationStateData(2,
                               _ActivelyWorkingState,
                               factory=cls._actively_working_situation_state),
            SituationStateData(3,
                               _BoredState,
                               factory=cls._bored_situation_state)
        ]

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return list(cls._arriving_situation_state._tuned_values.
                    job_and_role_changes.items())

    @classmethod
    def default_job(cls):
        pass

    def _get_role_state_overrides(self, sim, job_type, role_state_type,
                                  role_affordance_target):
        if self._cur_state is None:
            return (role_state_type, role_affordance_target)
        return self._cur_state._get_role_state_overrides(
            sim, job_type, role_state_type, role_affordance_target)

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

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        arrival_interaction = self.arrival_interaction
        staff_member = self.get_staff_member()
        if arrival_interaction is not None and staff_member is not None:
            interaction_context = InteractionContext(
                staff_member, InteractionContext.SOURCE_SCRIPT, Priority.Low)
            staffed_object = self.get_staffed_object()
            enqueue_result = staff_member.push_super_affordance(
                self.arrival_interaction, staffed_object, interaction_context)
            if not enqueue_result:
                logger.error(
                    'Failed to push the arrival interaction for the Staff Situation.'
                )

    def display_dialog(self, dialog_tuning):
        staff_member = self.get_staff_member()
        if dialog_tuning is not None and staff_member is not None:
            resolver = SingleSimResolver(staff_member)
            dialog = dialog_tuning(staff_member.sim_info, resolver=resolver)
            dialog.show_dialog()
Esempio n. 19
0
class ScheduledDeliveryLoot(HasTunableReference, HasTunableSingletonFactory, AutoFactoryInit, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.ACTION)):
    INSTANCE_TUNABLES = {'participant': TunableEnumEntry(description='\n            Sim who is getting the delivery delivered to their household.\n            ', tunable_type=ParticipantTypeSingleSim, default=ParticipantTypeSingleSim.Actor), 'time_from_now': TunableSimMinute(description='\n            How far from now we want our delivery.\n            ', default=1440, minimum=1, maximum=10080), 'not_home_notification': OptionalTunable(description='\n            If enabled, a notification will be displayed when the Sim is not\n            currently home when the object(s) would be delivered.\n            The object will be in the mailbox when they arrive back at their\n            home lot.\n            ', tunable=TunableUiDialogNotificationSnippet()), 'at_home_notification': OptionalTunable(description='\n            The notification that will be displayed when the Sim is at\n            home when the object(s) would be delivered. The object(s)\n            will end up in hidden inventory waiting to be delivered by\n            the mailman.\n            ', tunable=TunableUiDialogNotificationSnippet()), 'objects_to_deliver': ObjectRewardsOperation.TunableFactory(description='\n            The objects to be delivered. When participants are used \n            within this structure, only Sim-type participants will resolve.\n            ', locked_args={'notification': None, 'place_in_mailbox': True, 'force_family_inventory': False})}

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

    @flexmethod
    def apply_to_resolver(cls, inst, resolver, skip_test=False):
        subject = resolver.get_participant(cls.participant)
        subject.household.delivery_tracker.request_delivery(subject.sim_id, cls.guid64, create_time_span(minutes=cls.time_from_now))
Esempio n. 20
0
class MannequinComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=MANNEQUIN_COMPONENT, persistence_key=SimObjectAttributes_pb2.PersistenceMaster.PersistableData.MannequinComponent):
    MANNEQUIN_GROUP_SHARING_WARNING_NOTIFICATION = TunableUiDialogNotificationSnippet(description='\n        A notification to show explaining how outfit merging will clobber\n        outfits on the mannequin being placed into the world.\n        ', pack_safe=True)

    class _MannequinGroup(DynamicEnum):
        INVALID = 0

    class _MannequinTemplateExplicit(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {'age': TunableEnumEntry(description='\n                The default age of this object when placed from Build/Buy.\n                ', tunable_type=Age, default=Age.ADULT), 'gender': TunableEnumEntry(description='\n                The default gender of this object when placed from\n                Build/Buy.\n                ', tunable_type=Gender, default=Gender.MALE), 'skin_tone': TunableSkinTone(description='\n                The default skin tone of this object when placed from Build/Buy.\n                ')}

        def create_sim_info_data(self, sim_id):
            return SimInfoBaseWrapper(sim_id=sim_id, age=self.age, gender=self.gender, species=self.species, skin_tone=self.skin_tone)

    class _MannequinTemplateResource(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {'resource_key': TunableResourceKey(description='\n                The SimInfo file to use.\n                ', default=None, resource_types=(sims4.resources.Types.SIMINFO,)), 'outfit': OptionalTunable(description='\n                If enabled, the mannequin will default to the specified outfit.\n                ', tunable=TunableTuple(description='\n                    The outfit to switch the mannequin into.\n                    ', outfit_category=TunableEnumEntry(description='\n                        The outfit category.\n                        ', tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY), outfit_index=TunableRange(description='\n                        The outfit index.\n                        ', tunable_type=int, minimum=0, maximum=4, default=0)))}

        def create_sim_info_data(self, sim_id):
            sim_info_data = SimInfoBaseWrapper(sim_id=sim_id)
            sim_info_data.load_from_resource(self.resource_key)
            if self.outfit is not None:
                outfit_to_set = (self.outfit.outfit_category, self.outfit.outfit_index)
                if sim_info_data.has_outfit(outfit_to_set):
                    sim_info_data.set_current_outfit(outfit_to_set)
                    sim_info_data.set_previous_outfit(outfit_to_set, force=True)
            return sim_info_data

    FACTORY_TUNABLES = {'template': TunableVariant(description='\n            Define how the initial SimInfo data for this mannequin is created.\n            ', explicit=_MannequinTemplateExplicit.TunableFactory(), resource=_MannequinTemplateResource.TunableFactory(), default='resource'), 'cap_modifier': TunableRange(description='\n            This mannequin will be worth this many Sims when computing the cap\n            limit for NPCs in the world. While mannequins are not simulating\n            entities, they might have rendering costs that are equivalent to\n            those of Sims. We therefore need to limit how many of them are in\n            the world.\n            \n            Please consult Client Systems or CAS before changing this to a lower\n            number, as it might negatively impact performance, especially on\n            Minspec.\n            ', tunable_type=float, default=0.5, minimum=0, needs_tuning=False), 'outfit_sharing': OptionalTunable(description='\n            If enabled, all mannequins sharing the same group, age, and gender\n            will share outfits. Objects placed from the Gallery or the household\n            inventory will add any unique outfits to the master list, but will\n            lose any outfit beyond the maximum per category.\n            ', tunable=TunableEnumEntry(description='\n                The enum that controls how mannequins share outfits.\n                ', tunable_type=_MannequinGroup, default=_MannequinGroup.INVALID)), 'outfit_states': TunableMapping(description='\n            A mapping of outfit category to states. When the mannequin is\n            wearing the specified outfit category, it will transition into the\n            specified state.\n            ', key_type=TunableEnumEntry(description='\n                The outfit category that will trigger the associated state\n                change.\n                ', tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY), value_type=TunableStateValueReference(description='\n                The state of the object when the mannequin is wearing the\n                associated outfit category.\n                ')), 'outfit_modifiers': TunableMapping(description='\n            A mapping of modifiers to apply to the mannequin while in the\n            specified state.\n            ', key_type=TunableStateValueReference(description='\n                The state that triggers these outfit modifiers.\n                '), value_type=AppearanceModifier.TunableFactory(description='\n                An appearance modifier to apply while this state is active.\n                ')), 'state_trigger_grubby': TunableStateValueReference(description='\n            The state that triggers the mannequin being grubby. Any other state\n            on this track will set the mannequin as not grubby.\n            ', allow_none=True), 'state_trigger_singed': TunableStateValueReference(description='\n            The state that triggers the mannequin being singed. Any other state\n            on this track will set the mannequin as not singed.\n            ', allow_none=True)}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sim_info_data = None
        self._pose = None
        self._is_grubby = False
        self._is_singed = False

    @property
    def mannequin_id(self):
        return self.owner.id

    @distributor.fields.ComponentField(op=distributor.ops.ChangeSimOutfit)
    def mannequin_outfit(self):
        return self._sim_info_data.get_current_outfit()

    _resend_mannequin_outfit = mannequin_outfit.get_resend()

    @distributor.fields.ComponentField(op=distributor.ops.SetMannequinPose)
    def mannequin_pose(self):
        return self._pose

    @mannequin_pose.setter
    def mannequin_pose(self, value):
        self._pose = value

    @distributor.fields.ComponentField(op=distributor.ops.PreloadSimOutfit)
    def mannequin_outfit_preload_list(self):
        return self._sim_info_data.preload_outfit_list

    _resend_mannequin_outfit_preload_list = mannequin_outfit_preload_list.get_resend()

    @distributor.fields.ComponentField(op=distributor.ops.SetMannequinData, priority=distributor.fields.Field.Priority.HIGH)
    def sim_info_data(self):
        appearance_override_sim_info = self._sim_info_data.appearance_tracker.appearance_override_sim_info
        if appearance_override_sim_info is not None:
            return appearance_override_sim_info
        return self._sim_info_data

    _resend_sim_info_data = sim_info_data.get_resend()

    @distributor.fields.ComponentField(op=distributor.ops.SetGrubby)
    def mannequin_is_grubby(self):
        return self._is_grubby

    @mannequin_is_grubby.setter
    def mannequin_is_grubby(self, value):
        self._is_grubby = value

    @distributor.fields.ComponentField(op=distributor.ops.SetSinged)
    def mannequin_is_singed(self):
        return self._is_singed

    @mannequin_is_singed.setter
    def mannequin_is_singed(self, value):
        self._is_singed = value

    @componentmethod
    def get_current_outfit(self):
        return self._sim_info_data.get_current_outfit()

    @componentmethod
    def get_previous_outfit(self):
        return self._sim_info_data.get_previous_outfit()

    @componentmethod
    def get_outfits(self):
        return self._sim_info_data.get_outfits()

    @componentmethod
    def set_current_outfit(self, outfit):
        return self._sim_info_data.set_current_outfit(outfit)

    def _on_outfit_change(self, *_, **__):
        self._resend_mannequin_outfit()
        self._update_outfit_state()

    def _on_outfit_generated(self, outfit_category, outfit_index):
        self._sim_info_data.set_outfit_flags(outfit_category, outfit_index, BodyTypeFlag.CLOTHING_ALL)
        mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data)
        mannequin_group_data.set_mannequin_data(self._sim_info_data)
        mannequin_group_data.reconcile_mannequin_data()

    def _on_preload_outfits_changed(self):
        self._resend_mannequin_outfit_preload_list()

    def _update_outfit_state(self):
        if self.owner.id:
            outfit_category = self.get_current_outfit()[0]
            outfit_state = self.outfit_states.get(outfit_category)
            if outfit_state is not None:
                self.owner.set_state(outfit_state.state, outfit_state)

    def on_add(self, *_, **__):
        sim_spawner_service = services.sim_spawner_service()
        sim_spawner_service.add_npc_cap_modifier(self.cap_modifier)
        zone = services.current_zone()
        if not zone.is_zone_loading and not self.owner.is_downloaded:
            self.reconcile_mannequin_data(is_add=True)
        self._update_outfit_state()

    def on_remove(self, *_, **__):
        sim_spawner_service = services.sim_spawner_service()
        sim_spawner_service.add_npc_cap_modifier(-self.cap_modifier)
        if self._sim_info_data is not None:
            self._sim_info_data.on_outfit_changed.remove(self._on_outfit_change)
            self._sim_info_data.on_outfit_generated.remove(self._on_outfit_generated)
            self._sim_info_data.on_preload_outfits_changed.remove(self._on_preload_outfits_changed)

    def on_state_changed(self, state, old_value, new_value, from_init):
        old_appearance_modifier = self.outfit_modifiers.get(old_value)
        if old_appearance_modifier is not None:
            self._sim_info_data.appearance_tracker.remove_appearance_modifiers(state, source=self)
        new_appearance_modifier = self.outfit_modifiers.get(new_value)
        if new_appearance_modifier is not None:
            self._sim_info_data.appearance_tracker.add_appearance_modifiers(new_appearance_modifier.appearance_modifiers, state, new_appearance_modifier.priority, new_appearance_modifier.apply_to_all_outfits, source=self)
        if self.state_trigger_singed is not None:
            if state is self.state_trigger_singed.state:
                if new_value is self.state_trigger_singed:
                    self.mannequin_is_singed = True
                else:
                    self.mannequin_is_singed = False
        if self.state_trigger_grubby is not None:
            if state is self.state_trigger_grubby.state:
                if new_value is self.state_trigger_grubby:
                    self.mannequin_is_grubby = True
                else:
                    self.mannequin_is_grubby = False
        self._resend_sim_info_data()
        self._resend_mannequin_outfit()

    def pre_add(self, manager, obj_id):
        self._sim_info_data = self.template.create_sim_info_data(obj_id)
        self._sim_info_data.on_outfit_changed.append(self._on_outfit_change)
        self._sim_info_data.on_outfit_generated.append(self._on_outfit_generated)
        self._sim_info_data.on_preload_outfits_changed.append(self._on_preload_outfits_changed)
        if self.outfit_sharing is not None:
            mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data)
            mannequin_group_data.add_mannequin(self)

    def populate_sim_info_data_proto(self, sim_info_data_msg):
        sim_info_data_msg.mannequin_id = self.mannequin_id
        self._sim_info_data.save_sim_info(sim_info_data_msg)
        if self._pose is not None:
            sim_info_data_msg.animation_pose.asm = get_protobuff_for_key(self._pose.asm)
            sim_info_data_msg.animation_pose.state_name = self._pose.state_name

    def save(self, persistence_master_message):
        persistable_data = SimObjectAttributes_pb2.PersistenceMaster.PersistableData()
        persistable_data.type = SimObjectAttributes_pb2.PersistenceMaster.PersistableData.MannequinComponent
        mannequin_component_data = persistable_data.Extensions[SimObjectAttributes_pb2.PersistableMannequinComponent.persistable_data]
        if self._sim_info_data is not None:
            self.populate_sim_info_data_proto(mannequin_component_data.sim_info_data)
        persistence_master_message.data.extend((persistable_data,))

    def load(self, persistable_data):
        sim_info_data_proto = None
        persistence_service = services.get_persistence_service()
        if persistence_service is not None:
            sim_info_data_proto = persistence_service.get_mannequin_proto_buff(self.mannequin_id)
            if sim_info_data_proto is not None and self.outfit_sharing is not None:
                set_mannequin_group_data_reference(self.outfit_sharing, self._sim_info_data)
        if sim_info_data_proto is None:
            mannequin_component_data = persistable_data.Extensions[SimObjectAttributes_pb2.PersistableMannequinComponent.persistable_data]
            if mannequin_component_data.HasField('sim_info_data'):
                sim_info_data_proto = mannequin_component_data.sim_info_data
        if sim_info_data_proto is not None:
            self._sim_info_data.load_sim_info(sim_info_data_proto)
            persistence_service.del_mannequin_proto_buff(self.mannequin_id)
        zone = services.current_zone()
        if not zone.is_zone_loading:
            self.reconcile_mannequin_data(is_add=True, is_loaded=True)

    def on_finalize_load(self):
        self.reconcile_mannequin_data()

    def _replace_outfits(self, sim_info_data):
        current_outfit = self.get_current_outfit()
        default_outfit = (OutfitCategory.BATHING, 0)
        for (outfit_category, outfit_list) in sim_info_data.get_all_outfits():
            if outfit_category not in REGULAR_OUTFIT_CATEGORIES:
                continue
            self._sim_info_data.remove_outfits_in_category(outfit_category)
            for (outfit_index, outfit_data) in enumerate(outfit_list):
                source_outfit = (outfit_category, outfit_index)
                destination_outfit = self._sim_info_data.add_outfit(outfit_category, outfit_data)
                self._sim_info_data.generate_merged_outfit(sim_info_data, destination_outfit, default_outfit, source_outfit, preserve_outfit_flags=True)
        if self._sim_info_data.has_outfit(current_outfit):
            self._sim_info_data.set_current_outfit(current_outfit)
        else:
            self._sim_info_data.set_current_outfit(default_outfit)

    def _resend_mannequin_data(self):
        self._resend_sim_info_data()
        self._resend_mannequin_outfit()

    def reconcile_mannequin_data(self, *args, **kwargs):
        self.reconcile_mannequin_data_internal(*args, **kwargs)
        enable_mannequin_group_sharing_warning_notification()
        self._resend_mannequin_data()

    def reconcile_mannequin_data_internal(self, is_add=False, is_loaded=False):
        if self.outfit_sharing is None:
            return
        mannequin_group_sharing_mode = get_mannequin_group_sharing_mode()
        mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data)
        if is_loaded:
            if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_THEIRS:
                mannequin_group_data.set_mannequin_data(self._sim_info_data)
                for mannequin in mannequin_group_data:
                    if mannequin is not self:
                        mannequin._replace_outfits(self._sim_info_data)
                        mannequin._resend_mannequin_data()
                return
            if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_YOURS:
                self._replace_outfits(mannequin_group_data.get_mannequin_data())
                return
        mannequin_data = mannequin_group_data.get_mannequin_data()
        if mannequin_data is None:
            mannequin_group_data.set_mannequin_data(self._sim_info_data)
            if is_add:
                mannequin_group_data.reconcile_mannequin_data()
                return
        else:
            if is_add:
                if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_MERGED:
                    for (outfit_category, outfit_list) in self._sim_info_data.get_all_outfits():
                        if outfit_category not in REGULAR_OUTFIT_CATEGORIES:
                            continue
                        for (outfit_index, outfit_data) in enumerate(outfit_list):
                            if mannequin_data.is_generated_outfit_duplicate_in_category(self._sim_info_data, (outfit_category, outfit_index)):
                                continue
                            outfits_in_category = mannequin_data.get_outfits_in_category(outfit_category)
                            if outfits_in_category is not None and len(outfits_in_category) >= get_maximum_outfits_for_category(outfit_category):
                                show_mannequin_group_sharing_warning_notification()
                            else:
                                mannequin_data.generate_merged_outfit(self._sim_info_data, mannequin_data.add_outfit(outfit_category, outfit_data), mannequin_data.get_current_outfit(), (outfit_category, outfit_index), preserve_outfit_flags=True)
                mannequin_group_data.reconcile_mannequin_data()
                return
            if self.owner.id:
                for outfit_category in REGULAR_OUTFIT_CATEGORIES:
                    self._sim_info_data.generate_merged_outfits_for_category(mannequin_data, outfit_category, preserve_outfit_flags=True)
Esempio n. 21
0
class AdventureMoment(HasTunableFactory, AutoFactoryInit):
    LOOT_NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to recursively build loot notification text. It will be\n        given two tokens: a loot display text string, if any, and the previously\n        built LOOT_NOTIFICATION_TEXT string.\n        ')
    NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to format notifications. It will be given two arguments:\n        the notification text and the built version of LOOT_NOTIFICATION_TEXT,\n        if not empty.\n        ')
    CHEAT_TEXT = TunableTuple(description='\n        Strings to be used for display text on cheat buttons to trigger all\n        adventure moments. \n        ', previous_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on previous cheat button.\n            '), next_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on next cheat button.\n            '), text_pattern=TunableLocalizedStringFactory(description='\n            Format for displaying next and previous buttons text including the\n            progress.\n            '), tooltip=TunableLocalizedStringFactory(description='\n            Tooltip to show when disabling previous or next button.\n            '))
    COST_TYPE_SIMOLEONS = 0
    COST_TYPE_ITEMS = 1
    CHEAT_PREVIOUS_INDEX = 1
    CHEAT_NEXT_INDEX = 2
    FACTORY_TUNABLES = {'description': '\n            A phase of an adventure. Adventure moments may present\n            some information in a dialog form and for a choice to be\n            made regarding how the overall adventure will branch.\n            ', '_visibility': OptionalTunable(description='\n            Control whether or not this moment provides visual feedback to\n            the player (i.e., a modal dialog).\n            ', tunable=UiDialog.TunableFactory(), disabled_name='not_visible', enabled_name='show_dialog'), '_finish_actions': TunableList(description='\n            A list of choices that can be made by the player to determine\n            branching for the adventure. They will be displayed as buttons\n            in the UI. If no dialog is displayed, then the first available\n            finish action will be selected. If this list is empty, the\n            adventure ends.\n            ', tunable=TunableTuple(availability_tests=TunableTestSet(description='\n                    A set of tests that must pass in order for this Finish\n                    Action to be available on the dialog. A Finish Action failing\n                    all tests is handled as if it were never tuned.\n                    '), display_text=TunableLocalizedStringFactoryVariant(description="\n                   This finish action's title. This will be the button text in\n                   the UI.\n                   ", allow_none=True), display_subtext=TunableLocalizedStringFactoryVariant(description='\n                    If tuned, this text will display below the button for this Finish Action.\n                    \n                    Span tags can be used to change the color of the text to green/positive and red/negative.\n                    <span class="positive">TEXT</span> will make the word TEXT green\n                    <span class="negative">TEXT</span> will make the word TEXT red\n                    ', allow_none=True), disabled_text=OptionalTunable(description="\n                    If enabled, this is the string that will be displayed if \n                    this finishing action is not available because the tests \n                    don't pass.\n                    ", tunable=TunableLocalizedStringFactory()), cost=TunableVariant(description='\n                    The cost associated with this finish action. Only one type\n                    of cost may be tuned. The player is informed of the cost\n                    before making the selection by modifying the display_text\n                    string to include this information.\n                    ', simoleon_cost=TunableTuple(description="The specified\n                        amount will be deducted from the Sim's funds.\n                        ", locked_args={'cost_type': COST_TYPE_SIMOLEONS}, amount=TunableRange(description='How many Simoleons to\n                            deduct.\n                            ', tunable_type=int, default=0, minimum=0)), item_cost=TunableTuple(description="The specified items will \n                        be removed from the Sim's inventory.\n                        ", locked_args={'cost_type': COST_TYPE_ITEMS}, item_cost=ItemCost.TunableFactory()), default=None), action_results=TunableList(description='\n                    A list of possible results that can occur if this finish\n                    action is selected. Action results can award loot, display\n                    notifications, and control the branching of the adventure by\n                    selecting the next adventure moment to run.\n                    ', tunable=TunableTuple(weight_modifiers=TunableList(description='\n                            A list of modifiers that affect the probability that\n                            this action result will be chosen. These are exposed\n                            in the form (test, multiplier). If the test passes,\n                            then the multiplier is applied to the running total.\n                            The default multiplier is 1. To increase the\n                            likelihood of this action result being chosen, tune\n                            multiplier greater than 1. To decrease the\n                            likelihood of this action result being chose, tune\n                            multipliers lower than 1. If you want to exclude\n                            this action result from consideration, tune a\n                            multiplier of 0.\n                            ', tunable=TunableTuple(description='\n                                A pair of test and weight multiplier. If the\n                                test passes, the associated weight multiplier is\n                                applied. If no test is specified, the multiplier\n                                is always applied.\n                                ', test=TunableTestVariant(description='\n                                    The test that has to pass for this weight\n                                    multiplier to be applied. The information\n                                    available to this test is the same\n                                    information available to the interaction\n                                    owning this adventure.\n                                    ', test_locked_args={'tooltip': None}), weight_multiplier=Tunable(description='\n                                    The weight multiplier to apply if the\n                                    associated test passes.\n                                    ', tunable_type=float, default=1))), notification=OptionalTunable(description='\n                            If set, this notification will be displayed.\n                            ', tunable=TunableUiDialogNotificationSnippet()), next_moments=TunableList(description='\n                            A list of adventure moment keys. One of these keys will\n                            be selected to determine which adventure moment is\n                            selected next. If the list is empty, the adventure ends\n                            here. Any of the keys tuned here will have to be tuned\n                            in the _adventure_moments tunable for the owning adventure.\n                            ', tunable=AdventureMomentKey), loot_actions=TunableList(description='\n                            List of Loot actions that are awarded if this action result is selected.\n                            ', tunable=LootActions.TunableReference()), continuation=TunableContinuation(description='\n                            A continuation to push when running finish actions.\n                            '), results_dialog=OptionalTunable(description='\n                            A results dialog to show. This dialog allows a list\n                            of icons with labels.\n                            ', tunable=UiDialogLabeledIcons.TunableFactory()), events_to_send=TunableList(description='\n                            A list of events to send.\n                            ', tunable=TunableEnumEntry(description='\n                                events types to send\n                                ', tunable_type=TestEvent, default=TestEvent.Invalid))))))}

    def __init__(self, parent_adventure, **kwargs):
        super().__init__(**kwargs)
        self._parent_adventure = parent_adventure
        self.resolver = self._interaction.get_resolver()

    @property
    def _interaction(self):
        return self._parent_adventure.interaction

    @property
    def _sim(self):
        return self._interaction.sim

    def run_adventure(self):
        if self._visibility is None:
            if self._finish_actions:
                self._run_first_valid_finish_action()
        else:
            dialog = self._get_dialog()
            if dialog is not None:
                self._parent_adventure.force_action_result = True
                dialog.show_dialog(auto_response=0)

    def _run_first_valid_finish_action(self):
        resolver = self.resolver
        for (action_id, finish_action) in enumerate(self._finish_actions):
            if finish_action.availability_tests.run_tests(resolver):
                return self._run_action_from_index(action_id)

    def _is_action_result_available(self, action_result):
        if not action_result.next_moments:
            return True
        for moment_key in action_result.next_moments:
            if self._parent_adventure.is_adventure_moment_available(moment_key):
                return True
        return False

    def _run_action_from_cheat(self, action_index):
        cheat_index = action_index - len(self._finish_actions) + 1
        if cheat_index == self.CHEAT_PREVIOUS_INDEX:
            self._parent_adventure.run_cheat_previous_moment()
        elif cheat_index == self.CHEAT_NEXT_INDEX:
            self._parent_adventure.run_cheat_next_moment()

    def _get_action_result_weight(self, action_result):
        interaction_resolver = self.resolver
        weight = 1
        for modifier in action_result.weight_modifiers:
            if not modifier.test is None:
                if interaction_resolver(modifier.test):
                    weight *= modifier.weight_multiplier
            weight *= modifier.weight_multiplier
        return weight

    def _apply_action_cost(self, action):
        if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
            if not self._sim.family_funds.try_remove(action.cost.amount, Consts_pb2.TELEMETRY_INTERACTION_COST, sim=self._sim):
                return False
        elif action.cost.cost_type == self.COST_TYPE_ITEMS:
            item_cost = action.cost.item_cost
            return item_cost.consume_interaction_cost(self._interaction)()
        return True

    def _run_action_from_index(self, action_index):
        try:
            finish_action = self._finish_actions[action_index]
        except IndexError as err:
            logger.exception('Exception {} while attempting to get finish action.\nFinishActions length: {}, ActionIndex: {},\nCurrent Moment: {},\nResolver: {}.\n', err, len(self._finish_actions), action_index, self._parent_adventure._current_moment_key, self.resolver)
            return
        forced_action_result = False
        weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results if self._is_action_result_available(action_result)]
        if not weight_pairs:
            if self._parent_adventure.force_action_result:
                forced_action_result = True
                weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results]
        action_result = weighted_random_item(weight_pairs)
        if not (action_result is not None or not finish_action.action_results) and not self._apply_action_cost(finish_action):
            return
        if action_result is not None:
            loot_display_text = None
            resolver = self.resolver
            for actions in action_result.loot_actions:
                for (loot_op, test_ran) in actions.get_loot_ops_gen(resolver):
                    (success, _) = loot_op.apply_to_resolver(resolver, skip_test=test_ran)
                    if success and action_result.notification is not None:
                        current_loot_display_text = loot_op.get_display_text()
                        if current_loot_display_text is not None:
                            if loot_display_text is None:
                                loot_display_text = current_loot_display_text
                            else:
                                loot_display_text = self.LOOT_NOTIFICATION_TEXT(loot_display_text, current_loot_display_text)
            if action_result.notification is not None:
                if loot_display_text is not None:
                    notification_text = lambda *tokens: self.NOTIFICATION_TEXT(action_result.notification.text(*tokens), loot_display_text)
                else:
                    notification_text = action_result.notification.text
                dialog = action_result.notification(self._sim, self.resolver)
                dialog.text = notification_text
                dialog.show_dialog()
            if action_result.next_moments:
                if forced_action_result:
                    next_moment_key = random.choice(action_result.next_moments)
                else:
                    next_moment_key = random.choice(tuple(moment_key for moment_key in action_result.next_moments if self._parent_adventure.is_adventure_moment_available(moment_key)))
                self._parent_adventure.queue_adventure_moment(next_moment_key)
            if action_result.results_dialog:
                dialog = action_result.results_dialog(self._sim, resolver=self.resolver)
                dialog.show_dialog()
            event_manager = services.get_event_manager()
            for event_type in action_result.events_to_send:
                event_manager.process_event(event_type, sim_info=self._sim.sim_info)
            if action_result.continuation:
                self._interaction.push_tunable_continuation(action_result.continuation)

    def _is_cheat_response(self, response):
        cheat_response = response - len(self._finish_actions)
        if cheat_response < 0:
            return False
        return True

    def _on_dialog_response(self, dialog):
        response_index = dialog.response
        if response_index is None:
            return
        if False and self._is_cheat_response(response_index):
            self._run_action_from_cheat(response_index)
            return
        if response_index >= len(self._finish_actions):
            return
        self._run_action_from_index(response_index)

    def _get_action_display_text(self, action):
        display_name = self._interaction.create_localized_string(action.display_text)
        if action.cost is not None:
            if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
                amount = action.cost.amount
                display_name = self._interaction.SIMOLEON_COST_NAME_FACTORY(display_name, amount)
            elif action.cost.cost_type == self.COST_TYPE_ITEMS:
                item_cost = action.cost.item_cost
                display_name = item_cost.get_interaction_name(self._interaction, display_name)
        return lambda *_, **__: display_name

    def _get_dialog(self):
        resolver = self.resolver
        dialog = self._visibility(self._sim, resolver)
        responses = []
        for (action_id, finish_action) in enumerate(self._finish_actions):
            result = finish_action.availability_tests.run_tests(resolver)
            if not result:
                if finish_action.disabled_text is not None:
                    disabled_text = finish_action.disabled_text if not result else None
                    responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
            disabled_text = finish_action.disabled_text if not result else None
            responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
        if not responses:
            return
        if False and _show_all_adventure_moments:
            responses.extend(self._parent_adventure.get_cheat_responses(action_id))
        dialog.set_responses(responses)
        dialog.add_listener(self._on_dialog_response)
        return dialog
class TunedContinuousStatistic(statistics.continuous_statistic.ContinuousStatistic):
    INSTANCE_SUBCLASSES_ONLY = True
    INSTANCE_TUNABLES = {'decay_rate': sims4.tuning.tunable.TunableRange(description='\n            The decay rate for this stat (per sim minute).\n            ', tunable_type=float, default=0.001, minimum=0.0, tuning_group=GroupNames.CORE), '_decay_rate_overrides': TunableList(description='\n            A list of decay rate overrides.  Whenever the value of the stat\n            falls into this range, the decay rate is overridden with the value\n            specified. This overrides the base decay, so all decay modifiers\n            will still apply. The ranges are inclusive on the lower bound and\n            exclusive on the upper bound.  Overlapping values are not allowed\n            and will behave in an undefined manner.\n            ', tunable=TunableTuple(description='\n                The interval/decay_override pair.\n                ', interval=TunableInterval(description='\n                    The range at which this override will apply.  It is inclusive\n                    on the lower bound and exclusive on the upper bound.\n                    ', tunable_type=float, default_lower=-100, default_upper=100), decay_override=Tunable(description='\n                    The value that the base decay will be overridden with.\n                    ', tunable_type=float, default=0.0)), tuning_group=GroupNames.CORE), 'delayed_decay_rate': OptionalTunable(description="\n            When enabled contains the tuning for delayed decay. Delayed decay\n            is decay that happens if the value of the commodity hasn't changed\n            in some time.\n            ", tunable=TunableTuple(description='\n                All of the tuning for delayed decay rate.\n                ', initial_delay=TunableSimMinute(description='\n                    Time, in sim minutes, before the warning that a decay will\n                    start will be shown.\n                    ', default=30, minimum=0), final_delay=TunableSimMinute(description='\n                    Tim, in sim minutes, after the warning is shown that the\n                    decay will actually begin.\n                    ', default=30, minimum=0), delayed_decay_rate=sims4.tuning.tunable.TunableRange(description='\n                    The decay rate for this stat that starts after a delayed\n                    amount of time where the value of the skill does not change.\n                    ', tunable_type=float, default=0.001, minimum=0.0), decay_warning=OptionalTunable(description='\n                    If enabled, the notification to show to warn the user that\n                    a specific statistic is about to start decaying.\n                    ', tunable=TunableUiDialogNotificationSnippet(), enabled_by_default=True), decay_rate_overrides=TunableList(description='\n                    A list of decay rate overrides.  Whenever the value of the \n                    stat falls into this range, the decay rate is overridden \n                    with the value specified. This overrides the base decay, \n                    so all decay modifiers will still apply. The ranges are \n                    inclusive on the lower bound and exclusive on the upper \n                    bound.  Overlapping values are not allowed and will behave \n                    in an undefined manner.\n                    ', tunable=TunableTuple(description='\n                        The interval/decay_override pair.\n                        ', interval=TunableInterval(description='\n                            The range at which this override will apply.  It is \n                            inclusive on the lower bound and exclusive on the \n                            upper bound.\n                            ', tunable_type=float, default_lower=-100, default_upper=100), decay_override=Tunable(description='\n                            The value that the base decay will be overridden with.\n                            ', tunable_type=float, default=0.0), initial_delay_override=TunableSimMinute(description='\n                            The override for how long, in Sim Minutes, the \n                            initial delay is before the warning is given about \n                            decay starting.\n                            ', default=30, minimum=0), final_delay_override=TunableSimMinute(description='\n                            The override for how long, in Sim Minutes, the \n                            final delay is before the actual decay begins, \n                            after displaying the warning.\n                            ', default=30, minimum=0))), npc_decay=Tunable(description="\n                    By default decay doesn't happen for NPC sims. If enabled\n                    this will turn on decay of this statistic for NPC sims.\n                    ", tunable_type=bool, default=False)), tuning_group=GroupNames.CORE), '_default_convergence_value': Tunable(description='\n            The value toward which the stat decays.\n            ', tunable_type=float, default=0.0, tuning_group=GroupNames.CORE), 'stat_asm_param': statistics.tunable.TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.SPECIAL_CASES), 'min_value_tuning': Tunable(description='\n            The minimum value for this stat.\n            ', tunable_type=float, default=-100, tuning_group=GroupNames.CORE), 'max_value_tuning': Tunable(description='\n            The maximum value for this stat.\n            ', tunable_type=float, default=100, tuning_group=GroupNames.CORE), 'initial_value': Tunable(description='\n            The initial value for this stat.\n            ', tunable_type=float, default=0.0, tuning_group=GroupNames.CORE), 'persisted_tuning': Tunable(description="\n            Whether this statistic will persist when saving a Sim or an object.\n            For example, a Sims's SI score statistic should never persist.\n            ", tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'communicable_by_interaction_tag': OptionalTunable(description='\n            List of Tag and loot pairs that will trigger if either the actor or\n            target of an interaction has this statistic to give the first loot\n            whose tag matches any tag on the interaction.\n            \n            So you could do one loot for high risk socials, (tagged as such) a\n            different loot for low risk socials (tagged as such) a third loot\n            for high risk object interactions (licking bowl, maybe?), and\n            fourth loot for low risk object interaction\n            "generically using an object".\n            ', tunable=TunableList(tunable=TunableTuple(tag=TunableEnumEntry(description='\n                        Tag on interaction required to apply this loot.\n                        ', tunable_type=Tag, default=Tag.INVALID), loot=TunableReference(description='\n                        The loot to give.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',)))), tuning_group=GroupNames.SPECIAL_CASES), 'gallery_load_behavior': TunableEnumEntry(description="\n            When owner of commodity is loaded from the gallery, tune this to\n            determine if commodity should be loaded or not.\n            \n            DONT_LOAD = Don't load statistic when owner is coming from gallery\n            \n            LOAD_ONLY_FOR_OBJECT = Load only if statistic is being added to an\n            object.  If this statistic is tuned as a linked stat to a state,\n            make sure the state is also marked as gallery persisted. i.e.\n            Statistics like fish_freshness or gardening_groth. Switching on\n            this bit has performance implications when downloading a lot from\n            the gallery. Please discuss with a GPE when setting this tunable.\n    \n            LOAD_ONLY_FOR_SIM = Load only if statistic is being added to a sim.\n            LOAD_FOR_ALL = Always load commodity.  This has the same\n            ramifications as LOAD_ONLY_FOR_OBJECT if owner is an object.\n            ", tunable_type=GalleryLoadBehavior, default=GalleryLoadBehavior.LOAD_ONLY_FOR_SIM, tuning_group=GroupNames.SPECIAL_CASES)}

    @classmethod
    def _verify_tuning_callback(cls):
        if cls._decay_rate_overrides and cls.delayed_decay_rate is not None:
            logger.error('A Continous Statistic ({}) has tuned decay overrides \n            and tuned delayed decay rates. This is not supported. The override \n            will always be used and the delayed decay rate will never work. \n            Please choose one or the other or see a GPE if you really need this\n            to work for some reason. rfleig ', cls)

    def __init__(self, tracker, initial_value):
        super().__init__(tracker, initial_value)
        self._decay_override_calllback_handles = None
        if not self._tracker.suppress_callback_setup_during_load:
            self._create_new_override_callbacks()

    @sims4.utils.classproperty
    def max_value(cls):
        return cls.max_value_tuning

    @sims4.utils.classproperty
    def min_value(cls):
        return cls.min_value_tuning

    @sims4.utils.classproperty
    def best_value(cls):
        return cls.max_value

    def get_asm_param(self):
        return self.stat_asm_param.get_asm_param(self)

    @sims4.utils.classproperty
    def persisted(cls):
        return cls.persisted_tuning

    @sims4.utils.classproperty
    def persists_across_gallery_for_state(cls):
        if cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_FOR_ALL or cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_ONLY_FOR_OBJECT:
            return True
        return False

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._decay_override_list = cls._initialize_decay_override_list(cls._decay_rate_overrides, cls.decay_rate)
        if cls.delayed_decay_rate:
            cls._delayed_decay_override_list = cls._initialize_decay_override_list(cls.delayed_decay_rate.decay_rate_overrides, cls.delayed_decay_rate.delayed_decay_rate, delay=True)

    @classmethod
    def _initialize_decay_override_list(cls, override_tuning, default_decay, delay=False):
        if not override_tuning:
            return ()
        if delay:
            decay_override_list = [_DecayOverrideNode(override_data.interval.lower_bound, override_data.interval.upper_bound, override_data.decay_override, override_data.initial_delay_override, override_data.final_delay_override) for override_data in override_tuning]
        else:
            decay_override_list = [_DecayOverrideNode(override_data.interval.lower_bound, override_data.interval.upper_bound, override_data.decay_override) for override_data in override_tuning]
        decay_override_list.sort(key=lambda node: node.lower_bound)
        final_decay_override_list = []
        last_lower_bound = cls.max_value + 1
        for node in reversed(decay_override_list):
            if last_lower_bound > node.upper_bound:
                default_node = _DecayOverrideNode(node.upper_bound, last_lower_bound, default_decay)
                final_decay_override_list.insert(0, default_node)
            elif last_lower_bound < node.upper_bound:
                logger.error('Tuning error: two nodes are overlapping in continuous statistic decay overrides: {}', cls)
                node.upper_bound = last_lower_bound
            final_decay_override_list.insert(0, node)
            last_lower_bound = node.lower_bound
        if final_decay_override_list and final_decay_override_list[0].lower_bound > cls.min_value:
            default_node = _DecayOverrideNode(cls.min_value, final_decay_override_list[0].lower_bound, default_decay)
            final_decay_override_list.insert(0, default_node)
        return tuple(final_decay_override_list)

    def fixup_callbacks_during_load(self):
        super().fixup_callbacks_during_load()
        self._remove_decay_override_callbacks()
        self._create_new_override_callbacks()

    def _add_decay_override_callbacks(self, override_data, callback):
        if not override_data:
            return
        self._decay_override_calllback_handles = []
        value = self.get_value()
        for override in override_data:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    threshold = Threshold(override.lower_bound, operator.lt)
                    self._decay_override_calllback_handles.append(self.create_and_add_callback_listener(threshold, callback))
                    threshold = Threshold(override.upper_bound, operator.ge)
                    self._decay_override_calllback_handles.append(self.create_and_add_callback_listener(threshold, callback))
                    break

    def _remove_decay_override_callbacks(self):
        if not self._decay_override_calllback_handles:
            return
        for callback_listener in self._decay_override_calllback_handles:
            self.remove_callback_listener(callback_listener)
        self._decay_override_calllback_handles.clear()

    def _on_decay_rate_override_changed(self, _):
        value = self.get_value()
        self._remove_decay_override_callbacks()
        for override in self._decay_override_list:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    self._decay_rate_override = override.decay_override
                    self._create_new_override_callbacks()
                    return
        logger.error('No node found for stat value of {} on {}', value, self)

    def _on_delayed_decay_rate_override_changed(self, _):
        value = self.get_value()
        self._remove_decay_override_callbacks()
        for override in self._delayed_decay_override_list:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    self._delayed_decay_rate_override = override.decay_override
                    self._initial_delay_override = override.initial_delay_override
                    self._final_delay_override = override.final_delay_override
                    self._create_new_override_callbacks()
                    return
        logger.error('No node found for stat value of {} on {}', value, self)

    def _create_new_override_callbacks(self):
        if self._decay_rate_overrides:
            self._add_decay_override_callbacks(self._decay_override_list, self._on_decay_rate_override_changed)
        if self.delayed_decay_rate is not None and self.delayed_decay_rate.decay_rate_overrides:
            self._add_decay_override_callbacks(self._delayed_decay_override_list, self._on_delayed_decay_rate_override_changed)
        self._update_callback_listeners(resort_list=False)
Esempio n. 23
0
class LogicFestivalContestSituation(SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'contest_state':
        LogicFestivalContestState.TunableFactory(
            description=
            '\n            The contest state for this Situation\n            Starts.\n            ',
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP),
        'npc_job_and_role_state':
        TunableSituationJobAndRoleState(
            description=
            '\n            The job and role state of npcs that are in this situation.\n            '
        ),
        'player_job_and_role_state':
        TunableSituationJobAndRoleState(
            description=
            '\n            The job and role state of player Sims that are in this situation.\n            '
        ),
        'start_notification':
        OptionalTunable(
            description=
            '\n            If enabled then we will show a notification when this contest\n            begins.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification that will appear at the start of this situation.\n                '
            )),
        'end_notification':
        OptionalTunable(
            description=
            '\n            If enabled then we will show a notification when this contest ends.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification that will appear at the end of this situation.\n                '
            ))
    }
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def default_job(cls):
        pass

    @classmethod
    def _states(cls):
        return (SituationStateData(1,
                                   LogicFestivalContestState,
                                   factory=cls.contest_state), )

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.npc_job_and_role_state.job,
                 cls.npc_job_and_role_state.role_state),
                (cls.player_job_and_role_state.job,
                 cls.player_job_and_role_state.role_state)]

    def start_situation(self):
        super().start_situation()
        self._change_state(self.contest_state())
        if self.start_notification is not None:
            start_notification = self.start_notification(
                services.active_sim_info())
            start_notification.show_dialog()

    def _issue_requests(self):
        super()._issue_requests()
        request = SelectableSimRequestFactory(
            self,
            callback_data=_RequestUserData(
                role_state_type=self.player_job_and_role_state.role_state),
            job_type=self.player_job_and_role_state.job,
            exclusivity=self.exclusivity)
        self.manager.bouncer.submit_request(request)

    def _should_show_end_notification(self):
        return self.end_notification is not None

    def _get_end_notification_resolver_and_tokens(self):
        return (GlobalResolver(), tuple())

    def on_remove(self):
        if self._should_show_end_notification():
            (resolver, additional_tokens
             ) = self._get_end_notification_resolver_and_tokens()
            end_notification = self.end_notification(
                services.active_sim_info(), resolver=resolver)
            end_notification.show_dialog(additional_tokens=additional_tokens)
        super().on_remove()
class LifeSkillStatistic(HasTunableReference,
                         LifeSkillDisplayMixin,
                         TunedContinuousStatistic,
                         metaclass=HashedTunedInstanceMetaclass,
                         manager=services.get_instance_manager(
                             sims4.resources.Types.STATISTIC)):
    REMOVE_INSTANCE_TUNABLES = ('initial_value', )
    INSTANCE_TUNABLES = {
        'min_value_tuning':
        Tunable(description=
                '\n            The minimum value for this stat.\n            ',
                tunable_type=float,
                default=-100,
                export_modes=ExportModes.All),
        'max_value_tuning':
        Tunable(description=
                '\n            The maximum value for this stat.\n            ',
                tunable_type=float,
                default=100,
                export_modes=ExportModes.All),
        'initial_tuning':
        TunableLiteralOrRandomValue(
            description=
            '\n            The initial value of this stat.  Can be a single value or range.\n            ',
            tunable_type=float,
            default=0,
            minimum=-100),
        'initial_test_based_modifiers':
        TunableList(
            description=
            '\n            List of tuples containing test and a random value. If the test passes,\n            a random value is added to the already random initial value. \n            ',
            tunable=TunableTuple(
                description=
                '\n                A container for test and the corresponding random value.\n                ',
                initial_value_test=TunableTestSet(
                    description=
                    '\n                    If test passes, then the random value tuned will be applied\n                    to the initial value. \n                    '
                ),
                initial_modified_value=TunableLiteralOrRandomValue(
                    description=
                    '\n                    The initial value of this stat.  Can be a single value or range.\n                    ',
                    tunable_type=float,
                    default=0,
                    minimum=-100))),
        'age_to_remove_stat':
        TunableEnumEntry(
            description=
            '\n            When sim reaches this age, this stat will be removed permanently. \n            ',
            tunable_type=Age,
            default=Age.YOUNGADULT),
        'missing_career_decay_rate':
        Tunable(
            description=
            '\n            How much this life skill decay by if sim is late for school/work.\n            ',
            tunable_type=float,
            default=0.0),
        'trait_on_age_up_list':
        TunableList(
            description=
            '\n            A list of trait that will be applied on age up if this commodity \n            falls within the range specified in this tuple.\n            It also contains other visual information like VFX and notification.\n            ',
            tunable=TunableTuple(
                description=
                '\n                A container for the range and corresponding information.\n                ',
                export_class_name='TunableTraitOnAgeUpTuple',
                life_skill_range=TunableInterval(
                    description=
                    '\n                    If the commodity is in this range on age up, the trait\n                    will be applied. \n                    The vfx and notification will be played every time the \n                    range is crossed.\n                    ',
                    tunable_type=float,
                    default_lower=0,
                    default_upper=100,
                    export_modes=ExportModes.All),
                age_up_info=OptionalTunable(
                    description=
                    "\n                    If enabled, this trait will be added on age up given the specified age. \n                    Otherwise, no trait will be added.\n                    We don't use loot because UI needs this trait exported for display.\n                    ",
                    enabled_name='enabled_age_up_info',
                    tunable=TunableTuple(
                        export_class_name='TunableAgeUpInfoTuple',
                        age_to_apply_trait=TunableEnumEntry(
                            description=
                            '\n                            When sim reaches this age, this trait will be added on age up.\n                            ',
                            tunable_type=Age,
                            default=Age.YOUNGADULT),
                        life_skill_trait=Trait.TunableReference(
                            description=
                            '\n                            Trait that is added on age up.\n                            ',
                            pack_safe=True)),
                    export_modes=ExportModes.All),
                in_range_notification=
                OptionalTunable(tunable=TunableUiDialogNotificationSnippet(
                    description=
                    '\n                        Notification that is sent when the commodity reaches this range.\n                        '
                )),
                out_of_range_notification=
                OptionalTunable(tunable=TunableUiDialogNotificationSnippet(
                    description=
                    '\n                        Notification that is sent when the commodity exits this range.\n                        '
                )),
                vfx_triggered=TunablePlayEffectVariant(
                    description=
                    '\n                    Vfx to play on the sim when commodity enters this threshold.\n                    ',
                    tuning_group=GroupNames.ANIMATION),
                in_range_buff=OptionalTunable(tunable=TunableBuffReference(
                    description=
                    '\n                        Buff that is added when sim enters this threshold.\n                        '
                )))),
        'headline':
        TunableReference(
            description=
            '\n            The headline that we want to send down when this life skill updates.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.HEADLINE),
            tuning_group=GroupNames.UI)
    }

    def __init__(self, tracker):
        self._vfx = None
        super().__init__(tracker, self.get_initial_value())
        self._last_update_value = None
        if not tracker.load_in_progress:
            self._apply_initial_value_modifier()

    @classproperty
    def persists_across_gallery_for_state(cls):
        if cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_FOR_ALL or cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_ONLY_FOR_OBJECT:
            return True
        return False

    @classmethod
    def get_initial_value(cls):
        return cls.initial_tuning.random_int()

    def _apply_initial_value_modifier(self):
        initial_value = self._value
        resolver = SingleSimResolver(self.tracker.owner)
        for initial_modifier in self.initial_test_based_modifiers:
            if initial_modifier.initial_value_test.run_tests(resolver):
                initial_value += initial_modifier.initial_modified_value.random_float(
                )
        self.set_value(initial_value, from_add=True)

    def _update_value(self):
        old_value = self._value
        super()._update_value()
        new_value = self._value
        self._evaluate_threshold(old_value=old_value, new_value=new_value)

    def _evaluate_threshold(self, old_value=0, new_value=0, from_load=False):
        old_infos = []
        new_infos = []
        for range_info in self.trait_on_age_up_list:
            if old_value in range_info.life_skill_range:
                old_infos.append(range_info)
            if new_value in range_info.life_skill_range:
                new_infos.append(range_info)
        old_infos_set = set(old_infos)
        new_infos_set = set(new_infos)
        out_ranges = old_infos_set - new_infos_set
        in_ranges = new_infos_set - old_infos_set
        owner = self.tracker.owner
        is_household_sim = owner.is_selectable and owner.valid_for_distribution
        if not from_load:
            for out_range in out_ranges:
                if out_range.out_of_range_notification is not None and is_household_sim:
                    dialog = out_range.out_of_range_notification(
                        owner, resolver=SingleSimResolver(owner))
                    dialog.show_dialog(additional_tokens=(owner, ))
                if out_range.in_range_buff is not None:
                    owner.Buffs.remove_buff_by_type(
                        out_range.in_range_buff.buff_type)
        for in_range in in_ranges:
            if in_range.in_range_notification is not None and not from_load and is_household_sim:
                dialog = in_range.in_range_notification(
                    owner, resolver=SingleSimResolver(owner))
                dialog.show_dialog(additional_tokens=(owner, ))
            if in_range.vfx_triggered is not None and not from_load and is_household_sim:
                if self._vfx is not None:
                    self._vfx.stop(immediate=True)
                    self._vfx = None
                sim = owner.get_sim_instance(
                    allow_hidden_flags=ALL_HIDDEN_REASONS)
                if sim is not None:
                    self._vfx = in_range.vfx_triggered(sim)
                    self._vfx.start()
            if in_range.in_range_buff is not None:
                owner.Buffs.add_buff(
                    in_range.in_range_buff.buff_type,
                    buff_reason=in_range.in_range_buff.buff_reason)

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        self.create_and_send_commodity_update_msg(is_rate_change=False)

    @constproperty
    def remove_on_convergence():
        return False

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_value = self._value
        super().set_value(value,
                          *args,
                          from_load=from_load,
                          interaction=interaction,
                          **kwargs)
        new_value = self._value
        self._evaluate_threshold(old_value=old_value,
                                 new_value=new_value,
                                 from_load=from_load)
        if from_load:
            return
        self.create_and_send_commodity_update_msg(is_rate_change=False,
                                                  from_add=kwargs.get(
                                                      'from_add', False))

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        if self._vfx is not None:
            self._vfx.stop(immediate=True)
            self._vfx = None

    def save_statistic(self, commodities, skills, ranked_statistics, tracker):
        message = protocols.Commodity()
        message.name_hash = self.guid64
        message.value = self.get_saved_value()
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks(
            )
        commodities.append(message)

    def create_and_send_commodity_update_msg(self,
                                             is_rate_change=True,
                                             allow_npc=False,
                                             from_add=False):
        current_value = self.get_value()
        change_rate = self.get_change_rate()
        life_skill_msg = Commodities_pb2.LifeSkillUpdate()
        life_skill_msg.sim_id = self.tracker.owner.id
        life_skill_msg.life_skill_id = self.guid64
        life_skill_msg.curr_value = current_value
        life_skill_msg.rate_of_change = change_rate
        life_skill_msg.is_from_add = from_add
        send_sim_life_skill_update_message(self.tracker.owner, life_skill_msg)
        if self._last_update_value is None:
            value_to_send = change_rate
        else:
            value_to_send = current_value - self._last_update_value
        self._last_update_value = current_value
        if value_to_send != 0 and not from_add:
            self.headline.send_headline_message(self.tracker.owner,
                                                value_to_send)

    def create_and_send_life_skill_delete_msg(self):
        life_skill_msg = Commodities_pb2.LifeSkillDelete()
        life_skill_msg.sim_id = self.tracker.owner.id
        life_skill_msg.life_skill_id = self.guid64
        send_sim_life_skill_delete_message(self.tracker.owner, life_skill_msg)
Esempio n. 25
0
class PaymentElement(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'payment':
        TunablePaymentSnippet(),
        'display_only':
        Tunable(
            description=
            "\n            A PaymentElement marked as display_only will affect an affordance's\n            display name (by appending the Simoleon cost in parentheses), but\n            will not deduct funds when run.\n            ",
            tunable_type=bool,
            default=False),
        'include_in_total':
        Tunable(
            description=
            "\n            This should normally be set, but in cases where multiple payment\n            elements are tuned in separate outcomes, they will be all be summed\n            up to tally up the total cost of the interaction.\n            \n            In those cases, only set this to True for the 'definitive' cost.\n            ",
            tunable_type=bool,
            default=True),
        'insufficient_funds_behavior':
        TunableTuple(
            description=
            "\n            The behavior to define if we can succeed the payment if the\n            household doesn't have enough money.\n            ",
            allow_payment_succeed=Tunable(
                description=
                '\n                If True, the payment element will still return True if there is\n                not enough fund. Otherwise return False.\n                ',
                tunable_type=bool,
                default=False),
            notification=OptionalTunable(
                description=
                "\n                The notification about what the game will do if household\n                doesn't have enough fund.\n                ",
                tunable=TunableUiDialogNotificationSnippet()))
    }

    @classmethod
    def on_affordance_loaded_callback(cls,
                                      affordance,
                                      payment_element,
                                      object_tuning_id=DEFAULT):
        if not payment_element.include_in_total:
            return

        def get_simoleon_delta(interaction,
                               target=DEFAULT,
                               context=DEFAULT,
                               **interaction_parameters):
            interaction_resolver = interaction.get_resolver(
                target=target, context=context, **interaction_parameters)
            return payment_element.payment.get_simoleon_delta(
                interaction_resolver)

        def get_cost_upper_bound(funds_source=DEFAULT, context=DEFAULT):
            return payment_element.payment.payment_source.max_funds(
                context.sim)

        affordance.register_simoleon_delta_callback(
            get_simoleon_delta, object_tuning_id=object_tuning_id)
        affordance.register_upper_limit_callback(get_cost_upper_bound)
        affordance.register_cost_gain_strings_callbacks(
            payment_element.payment.get_cost_string,
            payment_element.payment.get_gain_string)

    def _do_behavior(self):
        if self.display_only:
            return True
        sim = self.interaction.sim
        resolver = self.interaction.get_resolver()
        if self.payment.try_deduct_payment(
                resolver, sim, self.try_show_insufficient_funds_notification):
            return True
        return self.insufficient_funds_behavior.allow_payment_succeed

    def try_show_insufficient_funds_notification(self):
        if self.insufficient_funds_behavior.notification is not None:
            sim = self.interaction.sim
            resolver = self.interaction.get_resolver()
            dialog = self.insufficient_funds_behavior.notification(
                sim, resolver)
            dialog.show_dialog()
Esempio n. 26
0
class Baby(GameObject):
    __qualname__ = 'Baby'
    MAX_PLACEMENT_ATTEMPTS = 8
    BABY_BASSINET_DEFINITION_MAP = TunableMapping(
        description=
        '\n        The corresponding mapping for each definition pair of empty bassinet\n        and bassinet with baby inside. The reason we need to have two of\n        definitions is one is deletable and the other one is not.\n        ',
        key_name='Baby',
        key_type=TunableReference(
            description=
            '\n            Bassinet with Baby object definition id.\n            ',
            manager=services.definition_manager()),
        value_name='EmptyBassinet',
        value_type=TunableReference(
            description=
            '\n            Bassinet with Baby object definition id.\n            ',
            manager=services.definition_manager()))
    BASSINET_EMPTY_STATE = TunableStateValueReference(
        description='\n        The state value for an empty bassinet.\n        '
    )
    BASSINET_BABY_STATE = TunableStateValueReference(
        description=
        '\n        The state value for a non-empty bassinet.\n        ')
    STATUS_STATE = ObjectState.TunableReference(
        description=
        '\n        The state defining the overall status of the baby (e.g. happy, crying,\n        sleeping). We use this because we need to reapply this state to restart\n        animations after a load.\n        '
    )
    BABY_SKIN_TONE_STATE_MAPPING = TunableMapping(
        description=
        '\n        From baby skin tone enum to skin tone state mapping.\n        ',
        key_type=TunableEnumEntry(tunable_type=BabySkinTone,
                                  default=BabySkinTone.MEDIUM),
        value_type=TunableTuple(boy=TunableStateValueReference(),
                                girl=TunableStateValueReference()))
    BABY_MOOD_MAPPING = TunableMapping(
        description=
        '\n        From baby state (happy, crying, sleep) to in game mood.\n        ',
        key_type=TunableStateValueReference(),
        value_type=Mood.TunableReference())
    BABY_SKIN_TONE_TO_CAS_SKIN_TONE = TunableMapping(
        description=
        '\n        From baby skin tone enum to cas skin tone id mapping.\n        ',
        key_type=TunableEnumEntry(tunable_type=BabySkinTone,
                                  default=BabySkinTone.MEDIUM),
        value_type=TunableList(
            description=
            '\n            The Skin Tones CAS reference under Catalog/InGame/CAS/Skintones.\n            ',
            tunable=TunableSkinTone()),
        export_modes=ExportModes.All,
        tuple_name='BabySkinToneToCasTuple')
    SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(
        description=
        '\n        The message appearing when a single baby is sent to daycare. You can\n        reference this single baby by name.\n        '
    )
    SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(
        description=
        '\n        The message appearing when multiple babies are sent to daycare. You can\n        not reference any of these babies by name.\n        '
    )
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet(
        description=
        '\n        The message appearing when a single baby is brought back from daycare.\n        You can reference this single baby by name.\n        '
    )
    BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet(
        description=
        '\n        The message appearing when multiple babies are brought back from\n        daycare. You can not reference any of these babies by name.\n        '
    )
    BABY_AGE_UP = TunableTuple(
        description=
        '\n        Multiple settings for baby age up moment.\n        ',
        age_up_affordance=TunableReference(
            description=
            '\n            The affordance to run when baby age up to kid.\n            ',
            manager=services.affordance_manager(),
            class_restrictions='SuperInteraction'),
        copy_states=TunableList(
            description=
            '\n            The list of the state we want to copy from the original baby\n            bassinet to the new bassinet to play idle.\n            ',
            tunable=TunableReference(manager=services.object_state_manager(),
                                     class_restrictions='ObjectState')),
        idle_state_value=TunableReference(
            description=
            '\n            The state value to apply on the new baby bassinet with the age up\n            special idle animation/vfx linked.\n            ',
            manager=services.object_state_manager(),
            class_restrictions='ObjectStateValue'))
    BABY_PLACEMENT_TAGS = TunableList(
        TunableEnumEntry(
            tag.Tag,
            tag.Tag.INVALID,
            description=
            '\n            Attempt to place the baby near objects with this tag set.\n            '
        ),
        description=
        '\n        When trying to place a baby bassinet on the lot, we attempt to place it\n        near other objects on the lot. Those objects are determined in priority\n        order by this tuned list. It will try to place next to all objects of the\n        matching types, before trying to place the baby in the middle of the lot,\n        and then finally trying the mailbox. If all FGL placements fail, we put\n        the baby into the household inventory.\n        '
    )
    BABY_THUMBNAIL_DEFINITION = TunableReference(
        description=
        '\n        The thumbnail definition for client use only.\n        ',
        manager=services.definition_manager(),
        export_modes=(ExportModes.ClientBinary, ))
    NEGLECTED_STATES = TunableList(
        description=
        '\n        If the baby enters any of these states, the neglected moment will begin.\n        ',
        tunable=TunableStateValueReference(
            description=
            '\n            The state to listen for in order to push the neglected moment on the baby.\n            '
        ))
    NEGLECT_ANIMATION = TunableReference(
        description=
        '\n        The animation to play on the baby for the neglect moment.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.ANIMATION),
        class_restrictions='ObjectAnimationElement')
    NEGLECT_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification to show when the baby neglect moment happens.\n        '
    )
    NEGLECT_EFFECT = Tunable(
        description=
        '\n        The VFX to play during the neglect moment.\n        ',
        tunable_type=str,
        default='s40_Sims_neglected')
    NEGLECT_EMPTY_BASSINET_STATE = TunableStateValueReference(
        description=
        '\n        The state that will be set on the bassinet when it is emptied due to\n        neglect. This should control any reaction broadcasters that we want to\n        happen when the baby is taken away. This MUST be tuned.\n        '
    )
    NEGLECT_BUFF_IMMEDIATE_FAMILY = TunableBuffReference(
        description=
        "\n        The buff to be applied to the baby's immediate family during the \n        neglect moment.\n        "
    )
    FAILED_PLACEMENT_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification to show if a baby could not be spawned into the world\n        because FGL failed. This is usually due to the player cluttering their\n        lot with objects. Token 0 is the baby.\n        '
    )

    @classmethod
    def get_baby_skin_tone_enum(cls, sim_info):
        if sim_info.is_baby:
            skin_tone_id = sim_info.skin_tone
            for (skin_enum,
                 tone_ids) in cls.BABY_SKIN_TONE_TO_CAS_SKIN_TONE.items():
                while skin_tone_id in tone_ids:
                    return skin_enum
            logger.error(
                'Baby with skin tone id {} not in BABY_SKIN_TONE_TO_CAS_SKIN_TONE. Setting light skin tone instead.',
                skin_tone_id,
                owner='jjacobson')
            return BabySkinTone.LIGHT
        return BabySkinTone.ADULT_SIM

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

    @classmethod
    def get_corresponding_definition(cls, definition):
        if definition in cls.BABY_BASSINET_DEFINITION_MAP:
            return cls.BABY_BASSINET_DEFINITION_MAP[definition]
        for (baby_def,
             bassinet_def) in cls.BABY_BASSINET_DEFINITION_MAP.items():
            while bassinet_def is definition:
                return baby_def

    @classmethod
    def get_default_baby_def(cls):
        return next(iter(cls.BABY_BASSINET_DEFINITION_MAP), None)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sim_info = None
        self.state_component.state_trigger_enabled = False
        self._started_neglect_moment = False
        self._ignore_daycare = False

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

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

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

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

    @property
    def household_id(self):
        if self._sim_info is not None:
            return self._sim_info.household.id
        return 0

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

    def enable_baby_state(self):
        if self._sim_info is None:
            return
        self.set_state(self.BASSINET_BABY_STATE.state,
                       self.BASSINET_BABY_STATE)
        status_state = self.get_state(self.STATUS_STATE)
        self.set_state(status_state.state, status_state, force_update=True)
        skin_tone_state = self.get_baby_skin_tone_state(self._sim_info)
        if skin_tone_state is not None:
            self.set_state(skin_tone_state.state, skin_tone_state)

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

    def on_state_changed(self, state, old_value, new_value):
        super().on_state_changed(state, old_value, new_value)
        if new_value in self.NEGLECTED_STATES and not self._started_neglect_moment:
            start_baby_neglect(self)
        elif self.manager is not None and new_value in self.BABY_MOOD_MAPPING:
            mood = self.BABY_MOOD_MAPPING[new_value]
            mood_msg = Commodities_pb2.MoodUpdate()
            mood_msg.sim_id = self.id
            mood_msg.mood_key = mood.guid64
            mood_msg.mood_intensity = 1
            distributor.shared_messages.add_object_message(
                self, MSG_SIM_MOOD_UPDATE, mood_msg, False)

    def load_object(self, object_data):
        super().load_object(object_data)
        self._sim_info = services.sim_info_manager().get(self.sim_id)

    def on_finalize_load(self):
        sim_info = services.sim_info_manager().get(self.sim_id)
        if sim_info is None or sim_info.household is not services.active_lot(
        ).get_household():
            _replace_bassinet(sim_info, bassinet=self)
        else:
            self.set_sim_info(sim_info)
Esempio n. 27
0
class Reward(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.REWARD)):
    __qualname__ = 'Reward'
    INSTANCE_SUBCLASSES_ONLY = True
    INSTANCE_TUNABLES = {'name': TunableLocalizedString(description='\n            The display name for this reward.\n            ', export_modes=ExportModes.All), 'reward_description': TunableLocalizedString(description='\n            Description for this reward.\n            ', export_modes=ExportModes.All), 'icon': TunableResourceKey(description='\n            The icon image for this reward.\n            ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), 'tests': TunableTestSet(description='\n            A series of tests that must pass in order for reward to be available.\n            '), 'rewards': TunableList(TunableVariant(description='\n                The gifts that will be given for this reward. They can be either\n                a specific reward or a random reward, in the form of a list of\n                specific rewards.\n                ', specific_reward=TunableSpecificReward(), random_reward=TunableList(TunableRandomReward()))), 'notification': OptionalTunable(description='\n            If enabled, this notification will show when the sim/household receives this reward.\n            ', tunable=TunableUiDialogNotificationSnippet())}

    @classmethod
    def give_reward(cls, sim_info):
        raise NotImplementedError

    @classmethod
    def try_show_notification(cls, sim_info):
        if cls.notification is not None:
            dialog = cls.notification(sim_info, SingleSimResolver(sim_info))
            dialog.show_dialog()

    @classmethod
    def is_valid(cls, sim_info):
        if not cls.tests.run_tests(SingleSimResolver(sim_info)):
            return False
        for reward in cls.rewards:
            if not isinstance(reward, tuple):
                return reward.valid_reward(sim_info)
            for each_reward in reward:
                while not each_reward.reward.valid_reward(sim_info):
                    return False
            return True
class ClubGatheringSituation(SituationComplexCommon):
    INSTANCE_TUNABLES = {'_default_job': SituationJob.TunableReference(description='\n            The default job for all members of this situation.\n            '), '_default_role_state': RoleState.TunableReference(description='\n            The Role State for Sims in the default job of this situation.\n            '), '_default_gathering_vibe': TunableEnumEntry(description='\n            The default Club vibe to use for the gathering.\n            ', tunable_type=ClubGatheringVibe, default=ClubGatheringVibe.NO_VIBE), '_vibe_buffs': TunableMapping(description="        \n            A Mapping of ClubGatheringVibe to List of buffs.\n            \n            When setting the vibe for the gathering the type is found in the\n            mapping and then each buff is processed in order until one of them\n            can be added. Then evaluation stops.\n            \n            Example: The club vibe is getting set to ClubGatheringVibe.Angry.\n            That entry has 3 buffs associated with it in the mapping. Angry3,\n            Angry2, Angry1 in that order. Angry3 doesn't pass evaluation so it\n            is passed. Next Angry2 does pass evaluation and so we add Angry2\n            Vibe Buff to the gathering. Angry1 is never evaluated in this\n            situation. Angry1 is only ever evaluated if Angry3 and Angry2 both\n            fail.\n            ", key_type=ClubGatheringVibe, value_type=TunableList(description='\n                A List of buff to attempt to use on the gathering. Order is\n                important as we do not try to give any buffs after one is given\n                to the gathering.\n                ', tunable=Buff.TunableReference(), minlength=1)), '_gathering_buff_reason': TunableLocalizedString(description='\n            The reason the gathering buff was added. Displayed on the buff\n            tooltip.\n            '), '_initial_disband_timer': TunableSimMinute(description='\n            The number of Sim minutes after a Gathering is created before it\n            can disband due to lack of members.\n            ', default=30, minimum=1), '_initial_notification': TunableUiDialogNotificationSnippet(description='\n            A notification that shows up once the gathering starts.\n            '), '_minimum_number_of_sims': TunableRange(description='\n            The minimum number of Sims that must be present in a Gathering to\n            keep it from disbanding.\n            ', tunable_type=int, default=3, minimum=2), 'time_between_bucks_rewards': TunableSimMinute(description='\n            The time in Sim Minutes to wait before awarding\n            the first club bucks for being in a gathering.\n            ', default=10), 'reward_bucks_per_interval': Tunable(description='\n            The amount of Club Bucks to award to the associated club at each \n            tuned interval.\n            ', tunable_type=int, default=1), 'rule_breaking_buff': TunableBuffReference(description='\n            Award this buff whenever a Sim breaks the rules.\n            ')}
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    def __init__(self, seed):
        super().__init__(seed)
        self.associated_club = None
        self._current_gathering_buff = None
        self._current_gathering_vibe = None
        self._sim_gathering_time_checks = {}
        self._can_disband = False
        self._initial_disband_timer_handle = None
        self._rewards_timer = None
        self._time_tracker_timer = None
        self._validity_household_id_override = None
        reader = self._seed.custom_init_params_reader
        if reader is not None:
            start_source = reader.read_uint64(ClubGatheringKeys.START_SOURCE, None)
            disband_ticks = reader.read_uint64(ClubGatheringKeys.DISBAND_TICKS, 0)
            self._validity_household_id_override = reader.read_uint64(ClubGatheringKeys.HOUSEHOLD_ID_OVERRIDE, None)
            associated_club_id = reader.read_uint64(ClubGatheringKeys.ASSOCIATED_CLUB_ID, None)
            if associated_club_id is not None:
                club_service = services.get_club_service()
                associated_club = club_service.get_club_by_id(associated_club_id)
                self.initialize_gathering(associated_club, disband_ticks=disband_ticks, start_source=start_source)
            current_gathering_buff_guid = reader.read_uint64(ClubGatheringKeys.GATHERING_BUFF, 0)
            self._current_gathering_buff = services.get_instance_manager(sims4.resources.Types.BUFF).get(current_gathering_buff_guid)
            vibe = reader.read_uint64(ClubGatheringKeys.GATHERING_VIBE, self._default_gathering_vibe)
            self.set_club_vibe(vibe)

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

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.default_job(), cls._default_role_state)]

    @classmethod
    def _states(cls):
        return (SituationStateData(1, ClubGatheringSituationState),)

    def _destroy(self):
        if self._initial_disband_timer_handle is not None:
            self._initial_disband_timer_handle.cancel()
            self._initial_disband_timer_handle = None
        if self._rewards_timer is not None:
            self._rewards_timer.cancel()
            self._rewards_timer = None
        if self._time_tracker_timer is not None:
            self._time_tracker_timer.cancel()
            self._time_tracker_timer = None
        super()._destroy()

    def _disband_timer_callback(self, _):
        self._can_disband = True
        if self._initial_disband_timer_handle is not None:
            alarms.cancel_alarm(self._initial_disband_timer_handle)
            self._initial_disband_timer_handle = None
        self._disband_if_neccessary()

    def _disband_if_neccessary(self):
        if not self._can_disband:
            return
        if len(list(self.all_sims_in_situation_gen())) < self._minimum_number_of_sims:
            self._self_destruct()

    def on_remove(self):
        self._can_disband = False
        super().on_remove()
        self._cleanup_gathering()

    def _cleanup_gathering(self):
        club_service = services.get_club_service()
        if club_service is None:
            logger.error("Attempting to end a Gathering but the ClubService doesn't exist.")
            return
        op = EndClubGathering(self.associated_club.club_id)
        Distributor.instance().add_op_with_no_owner(op)
        club_service.on_gathering_ended(self)

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

    def load_situation(self):
        result = super().load_situation()
        if result and (self.associated_club is None or not (not self.is_validity_overridden() and not self.associated_club.is_zone_valid_for_gathering(services.current_zone_id()))):
            self._cleanup_gathering()
            return False
        return result

    def is_validity_overridden(self):
        return self._validity_household_id_override == services.active_household_id() and services.active_household_lot_id() == services.active_lot_id()

    def initialize_gathering(self, associated_club, disband_ticks=None, start_source=None):
        club_service = services.get_club_service()
        if club_service is None:
            logger.error("Attempting to start a Gathering but the ClubService doesn't exist.")
            return
        self.associated_club = associated_club
        if start_source is not None:
            if start_source == ClubGatheringStartSource.APPLY_FOR_INVITE:
                invited_sim = services.sim_info_manager().get(self._guest_list.host_sim_id)
                self.associated_club.show_club_notification(invited_sim, ClubTunables.CLUB_GATHERING_START_DIALOG)
            elif any(sim_info.is_selectable for sim_info in self._guest_list.invited_sim_infos_gen()):
                initial_notification = self._initial_notification(services.active_sim_info())
                initial_notification.show_dialog(icon_override=IconInfoData(icon_resource=associated_club.icon), additional_tokens=(associated_club.name,))
            self._initial_disband_timer_handle = alarms.add_alarm(self.associated_club, interval_in_sim_minutes(self._initial_disband_timer), self._disband_timer_callback)
        elif disband_ticks > 0:
            self._initial_disband_timer_handle = alarms.add_alarm(self.associated_club, clock.TimeSpan(disband_ticks), self._disband_timer_callback)
        time_between_rewards = create_time_span(minutes=self.time_between_bucks_rewards)
        self._rewards_timer = alarms.add_alarm(self, time_between_rewards, self._award_club_bucks, True)
        time_between_gathering_checks = create_time_span(minutes=club_tuning.ClubTunables.MINUTES_BETWEEN_CLUB_GATHERING_PULSES)
        self._time_tracker_timer = alarms.add_alarm(self, time_between_gathering_checks, self._add_time_in_gathering, True)
        op = StartClubGathering(self.associated_club.club_id)
        Distributor.instance().add_op_with_no_owner(op)
        club_service.on_gathering_started(self)

    def _on_minimum_number_of_members_reached(self):
        self._can_disband = True
        if self._initial_disband_timer_handle is not None:
            alarms.cancel_alarm(self._initial_disband_timer_handle)
            self._initial_disband_timer_handle = None

    def _on_add_sim_to_situation(self, sim, *args, **kwargs):
        super()._on_add_sim_to_situation(sim, *args, **kwargs)
        club_service = services.get_club_service()
        if club_service is None:
            logger.error("Attempting to add a Sim to a Gathering but the ClubService doesn't exist.")
            return
        club_service.on_sim_added_to_gathering(sim, self)
        self.add_club_vibe_buff_to_sim(sim)
        relationship_tracker = sim.relationship_tracker
        relationship_tracker.add_create_relationship_listener(self._relationship_added_callback)
        sim.sim_info.register_for_outfit_changed_callback(self._on_outfit_changed)
        if not self._can_disband and len(list(self.all_sims_in_situation_gen())) >= self._minimum_number_of_sims:
            self._on_minimum_number_of_members_reached()
        op = UpdateClubGathering(GatheringUpdateType.ADD_MEMBER, self.associated_club.club_id, sim.id)
        Distributor.instance().add_op_with_no_owner(op)
        if self.associated_club.member_should_spin_into_club_outfit(sim):
            self._push_spin_into_current_outfit_interaction(sim)
        self._sim_gathering_time_checks[sim] = services.time_service().sim_timeline.now

    def _on_remove_sim_from_situation(self, sim):
        super()._on_remove_sim_from_situation(sim)
        sim.remove_buff_by_type(self._current_gathering_buff)
        self._disband_if_neccessary()
        club_service = services.get_club_service()
        if club_service is None:
            logger.error("Attempting to add a Sim to a Gathering but the ClubService doesn't exist.")
            return
        club_service.on_sim_removed_from_gathering(sim, self)
        relationship_tracker = sim.relationship_tracker
        relationship_tracker.remove_create_relationship_listener(self._relationship_added_callback)
        sim.sim_info.unregister_for_outfit_changed_callback(self._on_outfit_changed)
        if self.associated_club.member_should_spin_into_club_outfit(sim):
            sim.sim_info.register_for_outfit_changed_callback(self._on_outfit_removed)
            self._push_spin_into_current_outfit_interaction(sim)
        else:
            self._remove_apprearance_modifiers(sim.sim_info)
        if self.associated_club in club_service.clubs_to_gatherings_map:
            op = UpdateClubGathering(GatheringUpdateType.REMOVE_MEMBER, self.associated_club.club_id, sim.id)
            Distributor.instance().add_op_with_no_owner(op)
        if sim in self._sim_gathering_time_checks:
            self._process_time_in_gathering_event(sim)
            del self._sim_gathering_time_checks[sim]

    def set_club_vibe(self, vibe):
        self._current_gathering_vibe = vibe
        vibe_buffs = self._vibe_buffs.get(vibe, ())
        member = self.associated_club.members[0]
        for buff in vibe_buffs:
            if buff.can_add(member):
                if buff is not self._current_gathering_buff:
                    for sim in self.all_sims_in_situation_gen():
                        sim.remove_buff_by_type(self._current_gathering_buff)
                        self.add_club_vibe_buff_to_sim(sim, buff)
                    self._current_gathering_buff = buff
                return

    def add_club_vibe_buff_to_sim(self, sim, buff=None):
        buff = self._current_gathering_buff if buff is None else buff
        if buff is None:
            return
        if sim.has_buff(buff):
            return
        sim.add_buff(buff, self._gathering_buff_reason)

    def _relationship_added_callback(self, relationship):
        resolver = DoubleSimResolver(relationship.find_sim_info_a(), relationship.find_sim_info_b(), additional_participants={ParticipantType.AssociatedClub: (self.associated_club,)})
        for (perk, benefit) in ClubTunables.NEW_RELATIONSHIP_MODS.items():
            if self.associated_club.bucks_tracker.is_perk_unlocked(perk):
                if not benefit.test_set.run_tests(resolver=resolver):
                    continue
                benefit.loot.apply_to_resolver(resolver=resolver)

    def _award_club_bucks(self, handle):
        qualified_sims = [sim for sim in self._situation_sims if self._sim_satisfies_requirement_for_bucks(sim)]
        if not qualified_sims:
            return
        if any(sim for sim in self._situation_sims if club_tuning.ClubTunables.CLUB_BUCKS_REWARDS_MULTIPLIER.trait in sim.sim_info.trait_tracker.equipped_traits):
            multiplier = club_tuning.ClubTunables.CLUB_BUCKS_REWARDS_MULTIPLIER.multiplier
        else:
            multiplier = 1
        bucks_tracker = self.associated_club.bucks_tracker
        if bucks_tracker is None:
            return
        bucks_tracker.try_modify_bucks(ClubTunables.CLUB_BUCKS_TYPE, int(self.reward_bucks_per_interval*multiplier), reason='Time in club gathering')

    def _sim_satisfies_requirement_for_bucks(self, sim):
        if not sim.is_selectable:
            return False
        elif not sim.sim_info.is_instanced():
            return False
        return True

    def _on_outfit_changed(self, sim_info, outfit_category_and_index):
        club = self.associated_club
        (cas_parts_add, cas_parts_remove) = club.get_club_outfit_parts(sim_info, outfit_category_and_index)
        appearance_tracker = sim_info.appearance_tracker
        appearance_tracker.remove_appearance_modifiers(self.guid, source=self)
        modifiers = []
        for cas_part in cas_parts_add:
            modifier = AppearanceModifier.SetCASPart(cas_part=cas_part, should_toggle=False, replace_with_random=False, update_genetics=False, _is_combinable_with_same_type=True, remove_conflicting=False, outfit_type_compatibility=None)
            modifiers.append(modifier)
        for cas_part in cas_parts_remove:
            modifier = AppearanceModifier.SetCASPart(cas_part=cas_part, should_toggle=True, replace_with_random=False, update_genetics=False, _is_combinable_with_same_type=True, remove_conflicting=False, outfit_type_compatibility=None)
            modifiers.append(modifier)
        for modifier in modifiers:
            appearance_tracker.add_appearance_modifier(modifier, self.guid, 1, False, source=self)
        appearance_tracker.evaluate_appearance_modifiers()
        if sim_info.appearance_tracker.appearance_override_sim_info is not None:
            sim = sim_info.get_sim_instance()
            if sim is not None:
                sim.apply_outfit_buffs_for_sim_info(sim_info.appearance_tracker.appearance_override_sim_info, outfit_category_and_index)

    def _on_outfit_removed(self, sim_info, outfit_category_and_index):
        self._remove_apprearance_modifiers(sim_info)

    def _remove_apprearance_modifiers(self, sim_info):
        sim_info.appearance_tracker.remove_appearance_modifiers(self.guid, source=self)
        sim_info.unregister_for_outfit_changed_callback(self._on_outfit_removed)

    def _push_spin_into_current_outfit_interaction(self, sim):
        sim.sim_info.set_outfit_dirty(sim.get_current_outfit()[0])
        change_outfit_context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, priority.Priority.High)
        return sim.push_super_affordance(ForceChangeToCurrentOutfit, None, change_outfit_context)

    def remove_all_club_outfits(self):
        for sim in self.all_sims_in_situation_gen():
            self._push_spin_into_current_outfit_interaction(sim)

    def _add_time_in_gathering(self, handle):
        qualified_sims = [sim for sim in self._situation_sims if self._sim_satisfies_requirement_for_bucks(sim)]
        if not qualified_sims:
            return
        now = services.time_service().sim_timeline.now
        for sim in qualified_sims:
            self._process_time_in_gathering_event(sim, now)
            self._sim_gathering_time_checks[sim] = now

    def _process_time_in_gathering_event(self, sim, now=None):
        if now is None:
            now = services.time_service().sim_timeline.now
        elapsed_time = now - self._sim_gathering_time_checks[sim]
        services.get_event_manager().process_event(test_events.TestEvent.TimeInClubGathering, sim_info=sim.sim_info, amount=int(elapsed_time.in_minutes()))

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        writer.write_uint64(ClubGatheringKeys.ASSOCIATED_CLUB_ID, self.associated_club.club_id)
        if self._initial_disband_timer_handle is not None:
            current_time = services.time_service().sim_now
            disband_ticks = max((self._initial_disband_timer_handle.finishing_time - current_time).in_ticks(), 0)
        else:
            disband_ticks = 0
        writer.write_uint64(ClubGatheringKeys.DISBAND_TICKS, disband_ticks)
        if self._current_gathering_buff is not None:
            writer.write_uint64(ClubGatheringKeys.GATHERING_BUFF, self._current_gathering_buff.guid64)
        writer.write_uint64(ClubGatheringKeys.GATHERING_VIBE, self._current_gathering_vibe)
        if self._validity_household_id_override is not None:
            writer.write_uint64(ClubGatheringKeys.HOUSEHOLD_ID_OVERRIDE, self._validity_household_id_override)

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

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

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

    @property
    def _require_instanced_sim(self):
        return False

    @classproperty
    def persist_when_active(cls):
        return True

    def get_picker_schedule_time(self):
        return self._calculated_audition_time

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

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

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

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

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

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

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

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

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

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

    def on_calendar_alert_alarm(self):
        receiver_sim_info = self._receiver_sim_info
        resolver = SingleSimResolver(receiver_sim_info)
        dialog = self.advance_notice_notification(receiver_sim_info,
                                                  resolver=resolver)
        dialog.show_dialog()