예제 #1
0
class TravelTuning:
    ENTER_LOT_AFFORDANCE = TunableReference(services.affordance_manager(), description='SI to push when sim enters the lot.')
    EXIT_LOT_AFFORDANCE = TunableReference(services.affordance_manager(), description='SI to push when sim is exiting the lot.')
    NPC_WAIT_TIME = TunableSimMinute(15, description='Delay in sim minutes before pushing the ENTER_LOT_AFFORDANCE on a NPC at the spawn point if they have not moved.')
    TRAVEL_AVAILABILITY_SIM_FILTER = TunableSimFilter.TunableReference(description='Sim Filter to show what Sims the player can travel with to send to Game Entry.')
    TRAVEL_SUCCESS_AUDIO_STING = TunablePlayAudio(description='\n        The sound to play when we finish loading in after the player has traveled.\n        ')
    NEW_GAME_AUDIO_STING = TunablePlayAudio(description='\n        The sound to play when we finish loading in from a new game, resume, or\n        household move in.\n        ')
    GO_HOME_INTERACTION = TunableReference(description='\n        The interaction to push a Sim to go home.\n        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))
예제 #2
0
class ObjectCollectionData:
    __qualname__ = 'ObjectCollectionData'
    COLLECTIONS_DEFINITION = TunableList(
        description=
        '\n        List of collection groups.  Will need one defined per collection id\n        ',
        tunable=TunableCollectionTuple())
    COLLECTION_RARITY_MAPPING = TunableMapping(
        description=
        '\n            Mapping of collectible rarity to localized string for that rarity.\n            Used for displaying rarity names on the UI.',
        key_type=TunableEnumEntry(ObjectCollectionRarity,
                                  ObjectCollectionRarity.COMMON),
        value_type=TunableLocalizedString(
            description=
            '\n                Localization String For the name of the collection.  \n                This will be read on the collection UI to show item rarities.\n                '
        ))
    COLLECTION_COLLECTED_STING = TunablePlayAudio(
        description=
        '\n            The audio sting that gets played when a collectable is found.\n            '
    )
    COLLECTION_COMPLETED_STING = TunablePlayAudio(
        description=
        '\n            The audio sting that gets played when a collection is completed.\n            '
    )
    _COLLECTION_DATA = {}

    @classmethod
    def initialize_collection_data(cls):
        if not cls._COLLECTION_DATA:
            for collection_data in cls.COLLECTIONS_DEFINITION:
                for collectible_object in collection_data.object_list:
                    collectible_object._collection_id = collection_data.collection_id
                    cls._COLLECTION_DATA[collectible_object.collectable_item.
                                         id] = collectible_object

    @classmethod
    def get_collection_info_by_definition(cls, obj_def_id):
        if not cls._COLLECTION_DATA:
            ObjectCollectionData.initialize_collection_data()
        collectible = cls._COLLECTION_DATA.get(obj_def_id)
        if collectible:
            return (collectible._collection_id, collectible)
        return (None, None)

    @classmethod
    def get_collection_data(cls, collection_id):
        for collection_data in cls.COLLECTIONS_DEFINITION:
            while collection_data.collection_id == collection_id:
                return collection_data
예제 #3
0
class TunableAudioSting(XevtTriggeredElement, HasTunableFactory,
                        AutoFactoryInit):
    __qualname__ = 'TunableAudioSting'

    @staticmethod
    def _verify_tunable_callback(instance_class,
                                 tunable_name,
                                 source,
                                 audio_sting=None,
                                 **kwargs):
        if audio_sting is None:
            logger.error(
                "Attempting to play audio sting that hasn't been tuned on {}",
                source)

    FACTORY_TUNABLES = {
        'verify_tunable_callback':
        _verify_tunable_callback,
        'description':
        'Play an Audio Sting at the beginning/end of an interaction or on XEvent.',
        'audio_sting':
        TunablePlayAudio(
            description=
            '\n            The audio sting that gets played on the subject.\n            '
        ),
        'stop_audio_on_end':
        Tunable(
            description=
            "\n            If checked AND the timing is not set to END, the audio sting will\n            turn off when the interaction finishes. Otherwise, the audio will\n            play normally and finish when it's done.\n            ",
            tunable_type=bool,
            default=False),
        'subject':
        TunableEnumEntry(
            ParticipantType,
            ParticipantType.Actor,
            description='The participant who the audio sting will be played on.'
        )
    }

    def _build_outer_elements(self, sequence):
        def stop_audio(e):
            if hasattr(self, '_sound'):
                self._sound.stop()

        if self.stop_audio_on_end and self.timing is not self.AT_END:
            return build_element([sequence, stop_audio],
                                 critical=CleanupType.OnCancelOrException)
        return sequence

    def _do_behavior(self):
        subject = self.interaction.get_participant(self.subject)
        if subject is not None or not self.stop_audio_on_end:
            self._sound = play_tunable_audio(self.audio_sting, subject)
        else:
            logger.error(
                'Expecting to start and stop a TunableAudioSting during {} on a subject that is None.'
                .format(self.interaction),
                owner='rmccord')
예제 #4
0
class PlayAudioOp(BaseLootOperation):
    FACTORY_TUNABLES = {'audio': TunablePlayAudio(description='\n            The audio to play when this loot runs.\n            ')}

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

    def _apply_to_subject_and_target(self, subject, target, resolver):
        play_tunable_audio(self._audio)
예제 #5
0
class LotTuning(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.lot_tuning_manager()):
    INSTANCE_TUNABLES = {'walkby': situations.ambient.walkby_tuning.WalkbyTuning.TunableReference(allow_none=True), 'walkby_schedule': SchedulingWalkbyDirector.TunableReference(allow_none=True), 'audio_sting': OptionalTunable(description='\n                If enabled then the specified audio sting will play at the end\n                of the camera lerp when the lot is loaded.\n                ', tunable=TunablePlayAudio(description='\n                    The sound to play at the end of the camera lerp when the\n                    lot is loaded.\n                    ')), 'track_premade_status': Tunable(description="\n            If enabled, the lot will be flagged as no longer premade when the\n            player enters buildbuy on the lot or drops items/lots/rooms from\n            the gallery. Otherwise, the lot is still considered premade.\n            If disabled, the game won't care if this lot is premade or not.\n            \n            For example, the retail lots that were shipped with EP01 will track\n            the premade status so we know if objects should automatically be\n            set for sale.\n            ", tunable_type=bool, default=False)}
예제 #6
0
class ScreenSlam(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'display_type': ScreenSlamDisplayVariant(), 'title': OptionalTunable(description='\n            Title of the screen slam.\n            ', tunable=TunableLocalizedStringFactory()), 'text': OptionalTunable(description='"\n            Text of the screen slam.\n            ', tunable=TunableLocalizedStringFactory()), 'icon': OptionalTunable(description=',\n            Icon to be displayed for the screen Slam.\n            ', tunable=TunableIcon()), 'audio_sting': OptionalTunable(description='\n            A sting to play at the same time as the screen slam.\n            ***Some screen slams may appear to play a sting, but the audio is\n            actually tuned on something else.  Example: On CareerLevel tuning\n            there already is a tunable, Promotion Audio Sting, to trigger a\n            sting, so one is not necessary on the screen slam.  Make sure to\n            avoid having to stings play simultaneously.***\n            ', tunable=TunablePlayAudio()), 'active_sim_only': Tunable(description='\n            If true, the screen slam will be only be shown if the active Sim\n            triggers it.\n            ', tunable_type=bool, default=True)}

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

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

    @classmethod
    def get_aspiration(cls):
        pass

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

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

    def notify_gig_attended(self):
        self._gig_attended = True

    def has_attended_gig(self):
        return self._gig_attended

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

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

    def treat_work_time_as_due_date(self):
        return False

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

    def get_gig_time(self):
        return self._upcoming_gig_time

    def get_gig_customer(self):
        return self._customer_id

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

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

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

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

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

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

    def collect_rabbit_hole_rewards(self):
        pass

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

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

    def _determine_gig_outcome(self):
        raise NotImplementedError

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

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

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

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

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

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

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

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

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

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

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

    def _send_gig_telemetry(self, progress):
        with telemetry_helper.begin_hook(gig_telemetry_writer,
                                         TELEMETRY_HOOK_GIG_PROGRESS,
                                         sim_info=self._owner) as hook:
            hook.write_int(TELEMETRY_CAREER_ID, self.career.guid64)
            hook.write_int(TELEMETRY_GIG_ID, self.guid64)
            hook.write_int(TELEMETRY_GIG_PROGRESS_NUMBER, progress)
예제 #8
0
class UiDialog(HasTunableFactory, AutoFactoryInit):
    __qualname__ = 'UiDialog'
    DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
    FACTORY_TUNABLES = {
        'title':
        OptionalTunable(
            description=
            '\n            If enabled, this dialog will include title text.\n            ',
            tunable=TunableLocalizedStringFactory(
                description=
                "\n                The dialog's title.\n                ")),
        'text':
        TunableLocalizedStringFactoryVariant(
            description="\n            The dialog's text.\n            "),
        'text_tokens':
        OptionalTunable(
            description=
            '\n            If enabled, define text tokens to be used to localized text.\n            ',
            tunable=LocalizationTokens.TunableFactory(
                description=
                '\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '
            ),
            disabled_value=DEFAULT),
        'icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify an icon to be displayed.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'secondary_icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'phone_ring_type':
        TunableEnumEntry(
            description=
            '\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ',
            tunable_type=PhoneRingType,
            needs_tuning=True,
            default=PhoneRingType.NO_RING),
        'audio_sting':
        OptionalTunable(
            description=
            '\n            If enabled, play an audio sting when the dialog is shown.\n            ',
            tunable=TunablePlayAudio()),
        'ui_responses':
        TunableList(
            description=
            '\n            A list of buttons that are mapped to UI commands.\n            ',
            tunable=get_defualt_ui_dialog_response()),
        'dialog_options':
        TunableEnumFlags(
            description=
            '\n            Options to apply to the dialog.\n            ',
            enum_type=UiDialogOption,
            allow_no_flags=True,
            default=UiDialogOption.DISABLE_CLOSE_BUTTON)
    }

    def __init__(self, owner, resolver=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner.ref()
        self._resolver = resolver
        self._additional_responses = {}
        self.response = None
        self._timestamp = None
        self._listeners = CallableList()

    @property
    def accepted(self) -> bool:
        return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

    @property
    def responses(self):
        return tuple()

    @property
    def owner(self):
        return self._owner()

    @property
    def dialog_type(self):
        return self._dialog_type

    def add_listener(self, listener_callback):
        self._listeners.append(listener_callback)

    def set_responses(self, responses):
        self._additional_responses = tuple(responses)

    def has_responses(self):
        return self.responses or self._additional_responses

    def _get_responses_gen(self):
        yield self.responses
        yield self._additional_responses
        yield self.ui_responses

    def respond(self, response) -> bool:
        try:
            self.response = response
            self._listeners(self)
            return True
        finally:
            self.on_response_received()
        return False

    def update(self) -> bool:
        return True

    def show_dialog(self, on_response=None, **kwargs):
        if self.audio_sting is not None:
            play_tunable_audio(self.audio_sting, None)
        if on_response is not None:
            self.add_listener(on_response)
        pythonutils.try_highwater_gc()
        services.ui_dialog_service().dialog_show(self, self.phone_ring_type,
                                                 **kwargs)

    def distribute_dialog(self, dialog_type, dialog_msg):
        distributor = Distributor.instance()
        distributor.add_event(dialog_type, dialog_msg)

    def _build_localized_string_msg(self, string, *additional_tokens):
        if string is None:
            logger.callstack(
                '_build_localized_string_msg received None for the string to build. This is probably not intended.',
                owner='tingyul')
            return
        tokens = ()
        if self._resolver is not None:
            if self.text_tokens is DEFAULT:
                tokens = self._resolver.get_localization_tokens()
            elif self.text_tokens is not None:
                tokens = self.text_tokens.get_tokens(self._resolver)
        return string(*tokens + additional_tokens)

    def _build_response_arg(self,
                            response,
                            response_msg,
                            tutorial_id=None,
                            additional_tokens=(),
                            **kwargs):
        response_msg.choice_id = response.dialog_response_id
        response_msg.ui_request = response.ui_request
        if response.text is not None:
            response_msg.text = self._build_localized_string_msg(
                response.text, *additional_tokens)
        if tutorial_id is not None:
            response_msg.tutorial_args.tutorial_id = tutorial_id

    def build_msg(self,
                  additional_tokens=(),
                  icon_override=DEFAULT,
                  secondary_icon_override=DEFAULT,
                  **kwargs):
        msg = Dialog_pb2.UiDialogMessage()
        msg.dialog_id = self.dialog_id
        msg.owner_id = self.owner.id
        msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
        if self.title is not None:
            msg.title = self._build_localized_string_msg(
                self.title, *additional_tokens)
        msg.text = self._build_localized_string_msg(self.text,
                                                    *additional_tokens)
        if icon_override is DEFAULT:
            if self.icon is not None:
                icon_info = self.icon(self._resolver)
                key = icon_info[0]
                if key is not None:
                    msg.icon.type = key.type
                    msg.icon.group = key.group
                    msg.icon.instance = key.instance
                build_icon_info_msg(icon_info, None, msg.icon_info)
        elif icon_override is not None:
            build_icon_info_msg(icon_override, None, msg.icon_info)
        if secondary_icon_override is DEFAULT:
            if self.secondary_icon is not None:
                icon_info = self.secondary_icon(self._resolver)
                build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
        elif secondary_icon_override is not None:
            build_icon_info_msg(secondary_icon_override, None,
                                msg.secondary_icon_info)
        msg.dialog_options = self.dialog_options
        responses = []
        responses.extend(self._get_responses_gen())
        responses.sort(key=lambda response: response.sort_order)
        for response in responses:
            response_msg = msg.choices.add()
            self._build_response_arg(response,
                                     response_msg,
                                     additional_tokens=additional_tokens,
                                     **kwargs)
        return msg

    def on_response_received(self):
        pass

    def do_auto_respond(self):
        if ButtonType.DIALOG_RESPONSE_CANCEL in self.responses:
            response = ButtonType.DIALOG_RESPONSE_CANCEL
        elif ButtonType.DIALOG_RESPONSE_OK in self.responses:
            response = ButtonType.DIALOG_RESPONSE_OK
        else:
            response = ButtonType.DIALOG_RESPONSE_CLOSED
        services.ui_dialog_service().dialog_respond(self.dialog_id, response)
예제 #9
0
class UiDialog(UiDialogBase, HasTunableFactory, AutoFactoryInit):
	DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
	FACTORY_TUNABLES = {
		'title': OptionalTunable(description = '\n            If enabled, this dialog will include title text.\n            ', tunable = TunableLocalizedStringFactory(description = "\n                The dialog's title.\n                ")),
		'text': TunableLocalizedStringFactoryVariant(description = "\n            The dialog's text.\n            "),
		'text_tokens': OptionalTunable(description = '\n            If enabled, define text tokens to be used to localized text.\n            ', tunable = LocalizationTokens.TunableFactory(description = '\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '), disabled_value = DEFAULT),
		'icon': OptionalTunable(description = '\n            If enabled, specify an icon to be displayed.\n            ', tunable = TunableIconVariant()),
		'secondary_icon': OptionalTunable(description = '\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ', tunable = TunableIconVariant()),
		'phone_ring_type': TunableEnumEntry(description = '\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ', tunable_type = PhoneRingType, default = PhoneRingType.NO_RING),
		'anonymous_target_sim': Tunable(description = '\n            If this dialog is using a target sim id to give a conversation type view and this is checked, then the\n            target sim icon will instead be replaced by an anonymous caller.\n            ', tunable_type = bool, default = False),
		'audio_sting': OptionalTunable(description = '\n            If enabled, play an audio sting when the dialog is shown.\n            ', tunable = TunablePlayAudio()),
		'ui_responses': TunableList(description = '\n            A list of buttons that are mapped to UI commands.\n            ', tunable = get_defualt_ui_dialog_response(show_text = True)),
		'dialog_style': TunableEnumEntry(description = '\n            The style layout to apply to this dialog.\n            ', tunable_type = UiDialogStyle, default = UiDialogStyle.DEFAULT),
		'dialog_bg_style': TunableEnumEntry(description = '\n            The style background to apply to this dialog.\n            ', tunable_type = UiDialogBGStyle, default = UiDialogBGStyle.BG_DEFAULT),
		'dialog_options': TunableEnumFlags(description = '\n            Options to apply to the dialog.\n            ', enum_type = UiDialogOption, allow_no_flags = True, default = UiDialogOption.DISABLE_CLOSE_BUTTON),
		'timeout_duration': OptionalTunable(description = '\n            If enabled, override the timeout duration for this dialog in game\n            time.\n            ', tunable = TunableSimMinute(description = '\n                The time, in sim minutes, that this dialog should time out.\n                ', default = 5, minimum = 5)),
		'icon_override_participant': OptionalTunable(description = '\n            If enabled, allows a different participant to be considered the\n            owner of this dialog. Typically, this will only affect the Sim\n            portrait used at the top of the dialog, but there could be other\n            adverse affects so be sure to talk to your UI partner before tuning\n            this.\n            ', tunable = TunableEnumEntry(description = "\n                The participant to be used as the owner of this dialog. If this\n                participant doesn't exist, the default owner will be used\n                instead.\n                ", tunable_type = ParticipantTypeSingleSim, default = ParticipantTypeSingleSim.Invalid, invalid_enums = ParticipantTypeSingleSim.Invalid)),
		'additional_texts': OptionalTunable(description = '\n            If enabled, add additional text to the dialog\n            ', tunable = TunableList(tunable = TunableLocalizedStringFactory()))
	}

	def __init__ (self, owner, resolver = None, target_sim_id = None, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self._owner = owner.ref() if owner is not None else None
		self._resolver = resolver
		self._additional_responses = { }
		self._timestamp = None
		self._target_sim_id = target_sim_id

	@property
	def accepted (self) -> bool:
		return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

	@property
	def closed (self) -> bool:
		return self.response == ButtonType.DIALOG_RESPONSE_CLOSED

	@property
	def owner (self):
		if self._owner is not None:
			return self._owner()

	@property
	def dialog_type (self):
		return self._dialog_type

	def set_responses (self, responses):
		self._additional_responses = tuple(responses)

	def _get_responses_gen (self):
		yield from self.responses
		yield from self._additional_responses
		yield from self.ui_responses

	def get_phone_ring_type (self):
		return self.phone_ring_type

	def update (self) -> bool:
		return True

	def show_dialog (self, **kwargs):
		if self.audio_sting is not None:
			play_tunable_audio(self.audio_sting, None)
		if self.phone_ring_type == PhoneRingType.ALARM:
			return super().show_dialog(caller_id = self._owner().id, **kwargs)
		return super().show_dialog(caller_id = self._target_sim_id, **kwargs)

	def _build_localized_string_msg (self, string, *additional_tokens):
		if string is None:
			logger.callstack('_build_localized_string_msg received None for the string to build. This is probably not intended.', owner = 'tingyul')
			return
		tokens = ()
		if self._resolver is not None:
			if self.text_tokens is DEFAULT:
				tokens = self._resolver.get_localization_tokens()
			elif self.text_tokens is not None:
				tokens = self.text_tokens.get_tokens(self._resolver)
		return string(*tokens + additional_tokens)

	def _build_response_arg (self, response, response_msg, tutorial_id = None, additional_tokens = (), response_command_tuple = None, **kwargs):
		response_msg.choice_id = response.dialog_response_id
		response_msg.ui_request = response.ui_request
		if response.text is not None:
			response_msg.text = self._build_localized_string_msg(response.text, *additional_tokens)
		if response.subtext is not None:
			response_msg.subtext = response.subtext
		if response.disabled_text is not None:
			response_msg.disabled_text = response.disabled_text
		if tutorial_id is not None:
			response_msg.tutorial_args.tutorial_id = tutorial_id
		if response.response_command:
			response_msg.command_with_args.command_name = response.response_command.command
			for argument in response.response_command.arguments:
				with ProtocolBufferRollback(response_msg.command_with_args.command_remote_args.args) as entry:
					if argument.arg_type == CommandArgType.ARG_TYPE_SPECIAL:
						arg_type = response_command_tuple[0]
						arg_value = response_command_tuple[1]
					elif argument.arg_type == CommandArgType.ARG_TYPE_RESOLVED:
						(arg_type, arg_value) = argument.resolve_response_arg(self._resolver)
					else:
						arg_type = argument.arg_type
						arg_value = argument.arg_value
					if arg_type == CommandArgType.ARG_TYPE_BOOL:
						entry.bool = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_STRING:
						entry.string = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_FLOAT:
						entry.float = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_INT:
						entry.int64 = arg_value

	def build_msg (self, additional_tokens = (), icon_override = DEFAULT, secondary_icon_override = DEFAULT, text_override = DEFAULT, **kwargs):
		msg = Dialog_pb2.UiDialogMessage()
		msg.dialog_id = self.dialog_id
		msg.owner_id = self.owner.id if self.owner is not None else 0
		msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
		msg.dialog_style = self.dialog_style
		msg.dialog_bg_style = self.dialog_bg_style
		if self._target_sim_id is not None:
			msg.target_id = self._target_sim_id
		if self.title is not None:
			msg.title = self._build_localized_string_msg(self.title, *additional_tokens)
		if text_override is DEFAULT:
			msg.text = self._build_localized_string_msg(self.text, *additional_tokens)
		else:
			msg.text = self._build_localized_string_msg(text_override, *additional_tokens)
		if self.timeout_duration is not None:
			msg.timeout_duration = self.timeout_duration
		if icon_override is DEFAULT:
			if self.icon is not None:
				icon_info = self.icon(self._resolver)
				key = icon_info[0]
				if key is not None:
					msg.icon.type = key.type
					msg.icon.group = key.group
					msg.icon.instance = key.instance
				build_icon_info_msg(icon_info, None, msg.icon_info)
		elif icon_override is not None:
			build_icon_info_msg(icon_override, None, msg.icon_info)
		if secondary_icon_override is DEFAULT:
			if self.secondary_icon is not None:
				icon_info = self.secondary_icon(self._resolver)
				build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
		elif secondary_icon_override is not None:
			build_icon_info_msg(secondary_icon_override, None, msg.secondary_icon_info)
		if self.icon_override_participant is not None:
			msg.override_sim_icon_id = self._resolver.get_participants(self.icon_override_participant)[0].id
		msg.dialog_options = self.dialog_options
		msg.anonymous_target_sim = self.anonymous_target_sim
		responses = []
		responses.extend(self._get_responses_gen())
		responses.sort(key = lambda response: response.sort_order)
		for response in responses:
			response_msg = msg.choices.add()
			self._build_response_arg(response, response_msg, additional_tokens = additional_tokens, **kwargs)
		if self.additional_texts:
			for additional_text in self.additional_texts:
				msg.additional_texts.append(self._build_localized_string_msg(additional_text, *additional_tokens))
		return msg
class ActingStudioZoneDirector(CareerEventZoneDirector):
    INSTANCE_TUNABLES = {'stage_marks': TunableMapping(description='\n            A mapping of stage marker tags to the interactions that should be\n            added to them for this gig. These interactions will be applied to\n            the stage mark/object on zone load.\n            ', key_name='stage_mark_tag', key_type=TunableTag(description='\n                The tag for the stage mark object the tuned scene interactions\n                should be on.\n                ', filter_prefixes=('func',)), value_name='scene_interactions', value_type=TunableSet(description='\n                The set of interactions that will be added to the stage mark\n                object.\n                ', tunable=TunableReference(description='\n                    A Super Interaction that should be added to the stage mark\n                    object.\n                    ', manager=services.affordance_manager(), class_restrictions='SuperInteraction')), tuning_group=GroupNames.CAREER), 'performance_objects': TunableMapping(description='\n            A mapping of performance objects (i.e. lights, green screen, vfx\n            machine) and the state they should be put into when the performance\n            starts/stops.\n            ', key_name='performance_object_tag', key_type=TunableTag(description='\n                The tag for the performance object.\n                ', filter_prefixes=('func',)), value_name='performance_object_states', value_type=TunableTuple(description="\n                States that should be applied to the objects before, during, and\n                after the performance. If the object doesn't have the necessary\n                state then nothing will happen.\n                ", pre_performance_states=TunableSet(description='\n                    States to set on the object when the zone loads.\n                    ', tunable=TunableTuple(description='\n                        A state to set on an object as well as a perk that will\n                        skip setting the state.\n                        ', state_value=TunableReference(description='\n                            A state value to set on the object.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)), skip_with_perk=OptionalTunable(description='\n                            If enabled, allows skipping this state change if the\n                            active Sim has a tuned perk.\n                            ', tunable=TunableReference(description="\n                                If the active Sim has this perk, this state won't be\n                                set on the tuned objects. For instance, if the Sim\n                                has the Established Name perk, they don't need to\n                                use the hair and makeup chair. This can prevent\n                                those objects from glowing in that case.\n                                ", manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK))))), post_performance_states=TunableSet(description='\n                    States set on the object when the performance is over.\n                    ', tunable=TunableReference(description='\n                        A state value to set on the object.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',))), performance_states=TunableSet(description='\n                    States to set on the object when the performance starts.\n                    ', tunable=TunableReference(description='\n                        A state value to set on the object.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)))), tuning_group=GroupNames.CAREER), 'start_performance_interaction': TunableReference(description='\n            A reference to the interaction that indicates the performance is\n            starting. This is what triggers all of the state changes in the\n            Performance Object tuning.\n            ', manager=services.affordance_manager(), class_restrictions='SuperInteraction', tuning_group=GroupNames.CAREER), 'lot_load_loot': TunableMapping(description='\n            A mapping of Object IDs and loots to apply to those objects when the\n            lot loads. This can be used for things like applying specific locks\n            to door.\n            ', key_name='object_tag', key_type=TunableTag(description='\n                All objects with this tag will have the tuned loot applied on\n                lot load..\n                ', filter_prefixes=('func',)), value_name='loot', value_type=TunableSet(description='\n                A set of loots to apply to all objects with the specified tag.\n                ', tunable=TunableVariant(description='\n                    A specific loot to apply.\n                    ', lock_door=LockDoor.TunableFactory())), tuning_group=GroupNames.CAREER), 'thats_a_wrap_audio': TunablePlayAudio(description='\n            The sound to play when the player has completed the performance and\n            the Post Performance Time To Wrap Callout time has passed.\n            '), 'post_performance_time_remaining': TunableTimeSpan(description="\n            This is how long the gig should last once the player completes the\n            final interaction. Regardless of how long the timer shows, once the\n            player finishes the final interaction, we'll set the gig to end in\n            this tuned amount of time.\n            \n            Note: This should be enough time to encompass both the Post\n            Performance Time To Wrap Callout and Post Performance time Between\n            Wrap And Lights time spans.\n            ", default_minutes=20, locked_args={'days': 0}), 'post_performance_time_to_wrap_callout': TunableTimeSpan(description='\n            How long, after the Player completes the entire gig, until the\n            "That\'s a wrap" sound should play.\n            ', default_minutes=5, locked_args={'days': 0, 'hours': 0}), 'post_performance_time_between_wrap_and_lights': TunableTimeSpan(description='\n            How long after the "that\'s a wrap" sound until the post-performance\n            state should be swapped on all the objects (lights, greenscreen,\n            etc.)\n            ', default_minutes=5, locked_args={'days': 0, 'hours': 0})}
    ACTING_STUDIO_EVENTS = (TestEvent.InteractionComplete, TestEvent.MainSituationGoalComplete)
    STATE_PRE_PERFORMANCE = 0
    STATE_PERFORMANCE = 1
    STATE_POST_PERFORMANCE = 2
    SAVE_DATA_STATE = 'acting_studio_state'

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

    def _reset_data(self):
        self._stage_marks = set()
        self._performance_object_data = []
        self._post_performance_state_alarm = None
        self._post_performance_call_out_alarm = None
        self._current_state = self.STATE_PRE_PERFORMANCE

    def on_startup(self):
        super().on_startup()
        services.get_event_manager().register(self, self.ACTING_STUDIO_EVENTS)

    def on_cleanup_zone_objects(self):
        object_manager = services.object_manager()
        self._init_stage_marks(object_manager)
        self._init_performance_object_data(object_manager)
        self._apply_lot_load_loot(object_manager)

    def _apply_lot_load_loot(self, object_manager):
        active_sim_info = services.active_sim_info()
        for (tag, loots) in self.lot_load_loot.items():
            objects = object_manager.get_objects_matching_tags((tag,))
            for obj in objects:
                resolver = SingleActorAndObjectResolver(active_sim_info, obj, source=self)
                for loot in loots:
                    loot.apply_to_resolver(resolver)

    def on_shutdown(self):
        super().on_shutdown()
        services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS)
        self._reset_data()

    def on_career_event_stop(self):
        services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS)

    def handle_event(self, sim_info, event, resolver):
        career = services.get_career_service().get_career_in_career_event()
        if career.sim_info is not sim_info:
            return
        if event == TestEvent.InteractionComplete and isinstance(resolver.interaction, self.start_performance_interaction) and not resolver.interaction.has_been_reset:
            self._start_performance()
        elif event == TestEvent.MainSituationGoalComplete:
            self._end_performance(career)

    def _save_custom_zone_director(self, zone_director_proto, writer):
        writer.write_uint32(self.SAVE_DATA_STATE, self._current_state)
        super()._save_custom_zone_director(zone_director_proto, writer)

    def _load_custom_zone_director(self, zone_director_proto, reader):
        if reader is not None:
            self._current_state = reader.read_uint32(self.SAVE_DATA_STATE, self.STATE_PRE_PERFORMANCE)
        super()._load_custom_zone_director(zone_director_proto, reader)

    def _start_performance(self):
        for performance_object_data in self._performance_object_data:
            performance_object_data.set_performance_states()
        self._current_state = self.STATE_PERFORMANCE

    def _end_performance(self, career):
        new_end_time = services.time_service().sim_now + self.post_performance_time_remaining()
        career.set_career_end_time(new_end_time, reset_warning_alarm=False)
        self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_to_wrap_callout(), self._post_performance_wrap_callout)
        self._current_state = self.STATE_POST_PERFORMANCE

    def _post_performance_wrap_callout(self, _):
        play_tunable_audio(self.thats_a_wrap_audio)
        self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_between_wrap_and_lights(), self._post_performance_state_change)

    def _post_performance_state_change(self, _):
        for performance_object_data in self._performance_object_data:
            performance_object_data.set_post_performance_states()
        self._post_performance_state_alarm = None

    def _init_stage_marks(self, object_manager):
        for (tag, interactions) in self.stage_marks.items():
            marks = object_manager.get_objects_matching_tags((tag,))
            if not marks:
                continue
            self._stage_marks.update(marks)
            for obj in marks:
                obj.add_dynamic_component(types.STAGE_MARK_COMPONENT, performance_interactions=interactions)

    def _init_performance_object_data(self, object_manager):
        for (tag, states) in self.performance_objects.items():
            performance_objects = object_manager.get_objects_matching_tags((tag,))
            if not performance_objects:
                continue
            performance_object_data = PerformanceObjectData(performance_objects, states.pre_performance_states, states.performance_states, states.post_performance_states)
            self._performance_object_data.append(performance_object_data)
            if self._current_state == self.STATE_PRE_PERFORMANCE:
                performance_object_data.set_pre_performance_states()
예제 #11
0
class Bills:
    __qualname__ = 'Bills'
    BILL_ARRIVAL_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        A notification which pops up when bills are delivered.\n        '
    )
    UTILITY_INFO = TunableMapping(
        key_type=Utilities,
        value_type=TunableTuple(
            warning_notification=UiDialogNotification.TunableFactory(
                description=
                '\n                A notification which appears when the player will be losing this\n                utility soon due to delinquency.\n                '
            ),
            shutoff_notification=UiDialogNotification.TunableFactory(
                description=
                '\n                A notification which appears when the player loses this utility\n                due to delinquency.\n                '
            ),
            shutoff_tooltip=TunableLocalizedStringFactory(
                description=
                '\n                A tooltip to show when an interaction cannot be run due to this\n                utility being shutoff.\n                '
            )))
    BILL_COST_MODIFIERS = TunableMultiplier.TunableFactory(
        description=
        '\n        A tunable list of test sets and associated multipliers to apply to the total bill cost per payment.\n        '
    )
    BILL_OBJECT = TunableReference(
        description=
        "\n        The object that will be delivered to the lot's mailbox once bills have\n        been scheduled.\n        ",
        manager=services.definition_manager())
    DELINQUENCY_FREQUENCY = Tunable(
        description=
        '\n        Tunable representing the number of Sim hours between utility shut offs.\n        ',
        tunable_type=int,
        default=24)
    DELINQUENCY_WARNING_OFFSET_TIME = Tunable(
        description=
        '\n        Tunable representing the number of Sim hours before a delinquency state\n        kicks in that a warning notification pops up.\n        ',
        tunable_type=int,
        default=2)
    BILL_BRACKETS = TunableList(
        description=
        "\n        A list of brackets that determine the percentages that each portion of\n        a household's value is taxed at.\n        \n        ex: The first $2000 of a household's value is taxed at 10%, and\n        everything after that is taxed at 15%.\n        ",
        tunable=TunableTuple(
            description=
            '\n            A value range and tax percentage that define a bill bracket.\n            ',
            value_range=TunableInterval(
                description=
                "\n                A tunable range of integers that specifies a portion of a\n                household's total value.\n                ",
                tunable_type=int,
                default_lower=0,
                default_upper=None),
            tax_percentage=TunablePercent(
                description=
                "\n                A tunable percentage value that defines what percent of a\n                household's value within this value_range the player is billed\n                for.\n                ",
                default=10)))
    TIME_TO_PLACE_BILL_IN_HIDDEN_INVENTORY = TunableTimeOfWeek(
        description=
        "\n        The time of the week that we will attempt to place a bill in this\n        household's hidden inventory so it can be delivered.  This time should\n        be before the mailman shows up for that day or the bill will not be\n        delivered until the following day.\n        ",
        default_day=Days.MONDAY,
        default_hour=8,
        default_minute=0)
    AUDIO = TunableTuple(
        description=
        '\n        Tuning for all the audio stings that will play as a part of bills.\n        ',
        delinquency_warning_sfx=TunablePlayAudio(
            description=
            '\n            The sound to play when a delinquency warning is displayed.\n            '
        ),
        delinquency_activation_sfx=TunablePlayAudio(
            description=
            '\n            The sound to play when delinquency is activated.\n            '
        ),
        delinquency_removed_sfx=TunablePlayAudio(
            description=
            '\n            The sound to play when delinquency is removed.\n            '
        ),
        bills_paid_sfx=TunablePlayAudio(
            description=
            '\n            The sound to play when bills are paid.  If there are any delinquent\n            utilities, the delinquency_removed_sfx will play in place of this.\n            '
        ))

    def __init__(self, household):
        self._household = household
        self._utility_delinquency = {utility: False for utility in Utilities}
        self._can_deliver_bill = False
        self._current_payment_owed = None
        self._bill_timer_handle = None
        self._shutoff_handle = None
        self._warning_handle = None
        self._set_up_bill_timer()
        self._additional_bill_costs = {}
        self.bill_notifications_enabled = True
        self.autopay_bills = False
        self._bill_timer = None
        self._shutoff_timer = None
        self._warning_timer = None
        self._put_bill_in_hidden_inventory = False

    @property
    def can_deliver_bill(self):
        return self._can_deliver_bill

    @property
    def current_payment_owed(self):
        return self._current_payment_owed

    def _get_lot(self):
        home_zone = services.get_zone(self._household.home_zone_id)
        if home_zone is not None:
            return home_zone.lot

    def _set_up_bill_timer(self):
        day = self.TIME_TO_PLACE_BILL_IN_HIDDEN_INVENTORY.day
        hour = self.TIME_TO_PLACE_BILL_IN_HIDDEN_INVENTORY.hour
        minute = self.TIME_TO_PLACE_BILL_IN_HIDDEN_INVENTORY.minute
        time = create_date_and_time(days=day, hours=hour, minutes=minute)
        time_until_bill_delivery = services.time_service(
        ).sim_now.time_to_week_time(time)
        bill_delivery_time = services.time_service(
        ).sim_now + time_until_bill_delivery
        end_of_first_week = DateAndTime(0) + interval_in_sim_weeks(1)
        if bill_delivery_time < end_of_first_week:
            time_until_bill_delivery += interval_in_sim_weeks(1)
        if time_until_bill_delivery.in_ticks() <= 0:
            time_until_bill_delivery = TimeSpan(1)
        self._bill_timer_handle = alarms.add_alarm(
            self, time_until_bill_delivery,
            lambda _: self.allow_bill_delivery())

    def _set_up_timers(self):
        if self._bill_timer is None and self._shutoff_timer is None and self._warning_timer is None:
            return
        next_delinquent_utility = None
        for utility in self._utility_delinquency:
            if self._utility_delinquency[utility]:
                pass
            next_delinquent_utility = utility
            break
        if next_delinquent_utility is None:
            return

        def set_up_alarm(timer_data, handle, callback):
            if timer_data <= 0:
                return
            if handle is not None:
                alarms.cancel_alarm(handle)
            return alarms.add_alarm(self,
                                    clock.TimeSpan(timer_data),
                                    callback,
                                    use_sleep_time=False)

        warning_notification = self.UTILITY_INFO[
            next_delinquent_utility].warning_notification
        if self._bill_timer > 0 and self._current_payment_owed is not None:
            self._bill_timer_handle = set_up_alarm(
                self._bill_timer, self._bill_timer_handle,
                lambda _: self.allow_bill_delivery())
        self._shutoff_handle = set_up_alarm(
            self._shutoff_timer, self._shutoff_handle,
            lambda _: self._shut_off_utility(next_delinquent_utility))
        self._warning_handle = set_up_alarm(
            self._warning_timer, self._warning_handle,
            lambda _: self._send_notification(warning_notification))
        self._bill_timer = None
        self._shutoff_timer = None
        self._warning_timer = None

    def _destroy_timers(self):
        if self._bill_timer_handle is None and self._shutoff_handle is None and self._warning_handle is None:
            return
        current_time = services.time_service().sim_now
        if self._bill_timer_handle is not None:
            time = max((self._bill_timer_handle.finishing_time -
                        current_time).in_ticks(), 0)
            self._bill_timer = time
            alarms.cancel_alarm(self._bill_timer_handle)
            self._bill_timer_handle = None
        if self._shutoff_handle is not None:
            time = max((self._shutoff_handle.finishing_time -
                        current_time).in_ticks(), 0)
            self._shutoff_timer = time
            alarms.cancel_alarm(self._shutoff_handle)
            self._shutoff_handle = None
        if self._warning_handle is not None:
            time = max((self._warning_handle.finishing_time -
                        current_time).in_ticks(), 0)
            self._warning_timer = time
            alarms.cancel_alarm(self._warning_handle)
            self._warning_handle = None

    def on_all_households_and_sim_infos_loaded(self):
        active_household_id = services.active_household_id()
        if active_household_id is not None and self._household.id == active_household_id:
            self._set_up_timers()

    def on_client_disconnect(self):
        self._destroy_timers()

    def is_utility_delinquent(self, utility):
        if self._utility_delinquency[utility]:
            if self._current_payment_owed is None:
                self._clear_delinquency_status()
                logger.error(
                    'Household {} has delinquent utilities without actually owing any money. Resetting delinquency status.',
                    self._household,
                    owner='tastle')
                return False
            return True
        return False

    def is_any_utility_delinquent(self):
        for delinquency_status in self._utility_delinquency.values():
            while delinquency_status:
                return True
        return False

    def mailman_has_delivered_bills(self):
        if self.current_payment_owed is not None and (
                self._shutoff_handle is not None
                or self.is_any_utility_delinquent()):
            return True
        return False

    def is_additional_bill_source_delinquent(self, additional_bill_source):
        cost = self._additional_bill_costs.get(additional_bill_source, 0)
        if cost > 0 and any(self._utility_delinquency.values()):
            return True
        return False

    def test_utility_info(self, utility_info):
        if utility_info is None:
            return TestResult.TRUE
        for utility in utility_info:
            while utility in utility_info and self.is_utility_delinquent(
                    utility):
                return TestResult(
                    False,
                    'Bills: Interaction requires a utility that is shut off.',
                    tooltip=self.UTILITY_INFO[utility].shutoff_tooltip)
        return TestResult.TRUE

    def get_bill_amount(self):
        bill_amount = 0
        billable_household_value = self._household.household_net_worth(
            billable=True)
        for bracket in Bills.BILL_BRACKETS:
            lower_bound = bracket.value_range.lower_bound
            while billable_household_value >= lower_bound:
                upper_bound = bracket.value_range.upper_bound
                if upper_bound is None:
                    upper_bound = billable_household_value
                bound_difference = upper_bound - lower_bound
                value_difference = billable_household_value - lower_bound
                if value_difference > bound_difference:
                    value_difference = bound_difference
                value_difference *= bracket.tax_percentage
                bill_amount += value_difference
        for additional_cost in self._additional_bill_costs.values():
            bill_amount += additional_cost
        multiplier = 1
        for sim_info in self._household._sim_infos:
            multiplier *= Bills.BILL_COST_MODIFIERS.get_multiplier(
                SingleSimResolver(sim_info))
        bill_amount *= multiplier
        if bill_amount <= 0 and not self._household.is_npc_household:
            logger.error(
                'Player household {} has been determined to owe {} simoleons. Player households are always expected to owe at least some amount of money for bills.',
                self._household,
                bill_amount,
                owner='tastle')
        return int(bill_amount)

    def allow_bill_delivery(self):
        self._place_bill_in_hidden_inventory()

    def _place_bill_in_hidden_inventory(self):
        self._current_payment_owed = self.get_bill_amount()
        if self._current_payment_owed <= 0:
            self.pay_bill()
            return
        lot = self._get_lot()
        if lot is not None:
            lot.create_object_in_hidden_inventory(self.BILL_OBJECT)
            self._put_bill_in_hidden_inventory = False
            self._can_deliver_bill = True
            return
        self._put_bill_in_hidden_inventory = True
        self.trigger_bill_notifications_from_delivery()

    def _place_bill_in_mailbox(self):
        lot = self._get_lot()
        if lot is None:
            return
        lot.create_object_in_mailbox(self.BILL_OBJECT)
        self._put_bill_in_hidden_inventory = False

    def trigger_bill_notifications_from_delivery(self):
        if self.mailman_has_delivered_bills():
            return
        self._can_deliver_bill = False
        if self.autopay_bills or self._current_payment_owed == 0 or not self._household:
            self.pay_bill()
            return
        self._set_next_delinquency_timers()
        self._send_notification(self.BILL_ARRIVAL_NOTIFICATION)

    def pay_bill(self):
        if self._current_payment_owed:
            for status in self._utility_delinquency.values():
                while status:
                    play_tunable_audio(self.AUDIO.delinquency_removed_sfx)
                    break
            play_tunable_audio(self.AUDIO.bills_paid_sfx)
        self._current_payment_owed = None
        self._clear_delinquency_status()
        self._set_up_bill_timer()

        def remove_from_inventory(inventory):
            for obj in [
                    obj for obj in inventory
                    if obj.definition is self.BILL_OBJECT
            ]:
                obj.destroy(source=inventory, cause='Paying bills.')

        lot = self._get_lot()
        if lot is not None:
            for (_, inventory) in lot.get_all_object_inventories_gen():
                remove_from_inventory(inventory)
        for sim_info in self._household:
            sim = sim_info.get_sim_instance()
            while sim is not None:
                remove_from_inventory(sim.inventory_component)
        self._put_bill_in_hidden_inventory = False

    def _clear_delinquency_status(self):
        for utility in self._utility_delinquency:
            if utility == Utilities.POWER:
                self._start_all_power_utilities()
            self._utility_delinquency[utility] = False
        self._additional_bill_costs = {}
        if self._shutoff_handle is not None:
            alarms.cancel_alarm(self._shutoff_handle)
            self._shutoff_handle = None
        if self._warning_handle is not None:
            alarms.cancel_alarm(self._warning_handle)
            self._warning_handle = None
        for obj in services.object_manager().valid_objects():
            if obj.state_component is None:
                pass
            states_before_delinquency = obj.state_component.states_before_delinquency
            if not states_before_delinquency:
                pass
            for old_state in states_before_delinquency:
                obj.set_state(old_state.state, old_state)
            obj.state_component.states_before_delinquency = []

    def _set_next_delinquency_timers(self):
        for utility in self._utility_delinquency:
            if self._utility_delinquency[utility]:
                pass
            warning_notification = self.UTILITY_INFO[
                utility].warning_notification
            self._warning_handle = alarms.add_alarm(
                self,
                clock.interval_in_sim_hours(
                    self.DELINQUENCY_FREQUENCY -
                    self.DELINQUENCY_WARNING_OFFSET_TIME),
                lambda _: self._send_notification(warning_notification))
            self._shutoff_handle = alarms.add_alarm(
                self, clock.interval_in_sim_hours(self.DELINQUENCY_FREQUENCY),
                lambda _: self._shut_off_utility(utility))
            break

    def _shut_off_utility(self, utility):
        if self._current_payment_owed == None:
            self._clear_delinquency_status()
            logger.error(
                'Household {} is getting a utility shut off without actually owing any money. Resetting delinquency status.',
                self._household,
                owner='tastle')
            return
        shutoff_notification = self.UTILITY_INFO[utility].shutoff_notification
        self._send_notification(shutoff_notification)
        if self._shutoff_handle is not None:
            alarms.cancel_alarm(self._shutoff_handle)
            self._shutoff_handle = None
        self._utility_delinquency[utility] = True
        self._set_next_delinquency_timers()
        self._cancel_delinquent_interactions(utility)
        if utility == Utilities.POWER:
            self._stop_all_power_utilities()
        play_tunable_audio(self.AUDIO.delinquency_activation_sfx)

    def _cancel_delinquent_interactions(self, delinquent_utility):
        for sim in services.sim_info_manager().instanced_sims_gen():
            for interaction in sim.si_state:
                utility_info = interaction.utility_info
                if utility_info is None:
                    pass
                while delinquent_utility in utility_info:
                    interaction.cancel(
                        FinishingType.FAILED_TESTS,
                        'Bills. Interaction violates current delinquency state of household.'
                    )
        for obj in services.object_manager().valid_objects():
            if obj.state_component is None:
                pass
            delinquency_state_changes = obj.state_component.delinquency_state_changes
            while delinquency_state_changes is not None and delinquent_utility in delinquency_state_changes:
                new_states = delinquency_state_changes[delinquent_utility]
                if not new_states:
                    pass
                while True:
                    for new_state in new_states:
                        if obj.state_value_active(new_state):
                            pass
                        obj.state_component.states_before_delinquency.append(
                            obj.state_component.get_state(new_state.state))
                        obj.set_state(new_state.state, new_state)

    def _start_all_power_utilities(self):
        object_manager = services.object_manager()
        for light_obj in object_manager.get_all_objects_with_component_gen(
                objects.components.types.LIGHTING_COMPONENT):
            while light_obj.get_household_owner_id() == self._household.id:
                light_obj.lighting_component.on_power_on()

    def _stop_all_power_utilities(self):
        object_manager = services.object_manager()
        for light_obj in object_manager.get_all_objects_with_component_gen(
                objects.components.types.LIGHTING_COMPONENT):
            while light_obj.get_household_owner_id() == self._household.id:
                light_obj.lighting_component.on_power_off()

    def _send_notification(self, notification):
        if not self.bill_notifications_enabled:
            return
        client = services.client_manager().get_client_by_household(
            self._household)
        if client is not None:
            active_sim = client.active_sim
            if active_sim is not None:
                remaining_time = max(
                    int(self._shutoff_handle.get_remaining_time().in_hours()),
                    0)
                dialog = notification(active_sim, None)
                dialog.show_dialog(
                    additional_tokens=(remaining_time,
                                       self._current_payment_owed))
        current_time = services.time_service().sim_now
        if self._warning_handle is not None and self._warning_handle.finishing_time <= current_time:
            alarms.cancel_alarm(self._warning_handle)
            self._warning_handle = None
            play_tunable_audio(self.AUDIO.delinquency_warning_sfx)

    def add_additional_bill_cost(self, additional_bill_source, cost):
        current_cost = self._additional_bill_costs.get(additional_bill_source,
                                                       0)
        self._additional_bill_costs[
            additional_bill_source] = current_cost + cost

    def load_data(self, householdProto):
        for utility in householdProto.gameplay_data.delinquent_utilities:
            self._utility_delinquency[utility] = True
            while utility == Utilities.POWER:
                self._stop_all_power_utilities()
        for additional_bill_cost in householdProto.gameplay_data.additional_bill_costs:
            self.add_additional_bill_cost(additional_bill_cost.bill_source,
                                          additional_bill_cost.cost)
        self._can_deliver_bill = householdProto.gameplay_data.can_deliver_bill
        self._put_bill_in_hidden_inventory = householdProto.gameplay_data.put_bill_in_hidden_inventory
        if self._put_bill_in_hidden_inventory:
            self._place_bill_in_mailbox()
        self._current_payment_owed = householdProto.gameplay_data.current_payment_owed
        if self._current_payment_owed == 0:
            self._current_payment_owed = None
        self._bill_timer = householdProto.gameplay_data.bill_timer
        self._shutoff_timer = householdProto.gameplay_data.shutoff_timer
        self._warning_timer = householdProto.gameplay_data.warning_timer
        active_household_id = services.active_household_id()
        if active_household_id is not None and self._household.id == active_household_id:
            self._set_up_timers()
        elif self._bill_timer_handle is not None:
            alarms.cancel_alarm(self._bill_timer_handle)
            self._bill_timer_handle = None

    def save_data(self, household_msg):
        for utility in Utilities:
            while self.is_utility_delinquent(utility):
                household_msg.gameplay_data.delinquent_utilities.append(
                    utility)
        for (bill_source, cost) in self._additional_bill_costs.items():
            with ProtocolBufferRollback(
                    household_msg.gameplay_data.additional_bill_costs
            ) as additional_bill_cost:
                additional_bill_cost.bill_source = bill_source
                additional_bill_cost.cost = cost
        household_msg.gameplay_data.can_deliver_bill = self._can_deliver_bill
        household_msg.gameplay_data.put_bill_in_hidden_inventory = self._put_bill_in_hidden_inventory
        if self.current_payment_owed is not None:
            household_msg.gameplay_data.current_payment_owed = self.current_payment_owed
        current_time = services.time_service().sim_now
        if self._bill_timer_handle is not None:
            time = max((self._bill_timer_handle.finishing_time -
                        current_time).in_ticks(), 0)
            household_msg.gameplay_data.bill_timer = time
        elif self._bill_timer is not None:
            household_msg.gameplay_data.bill_timer = self._bill_timer
        if self._shutoff_handle is not None:
            time = max((self._shutoff_handle.finishing_time -
                        current_time).in_ticks(), 0)
            household_msg.gameplay_data.shutoff_timer = time
        elif self._shutoff_timer is not None:
            household_msg.gameplay_data.shutoff_timer = self._shutoff_timer
        if self._warning_handle is not None:
            time = max((self._warning_handle.finishing_time -
                        current_time).in_ticks(), 0)
            household_msg.gameplay_data.warning_timer = time
        elif self._warning_timer is not None:
            household_msg.gameplay_data.warning_timer = self._warning_timer
예제 #12
0
class ObjectCollectionData:
    COLLECTIONS_DEFINITION = TunableList(
        description=
        '\n        List of collection groups.  Will need one defined per collection id\n        ',
        tunable=TunableCollectionTuple())
    COLLECTION_RARITY_MAPPING = TunableMapping(
        description=
        '\n        Mapping of collectible rarity to localized string for that rarity.\n        Used for displaying rarity names on the UI.\n        ',
        key_type=TunableReference(
            description=
            '\n            Mapping of rarity state to text\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.OBJECT_STATE),
            needs_tuning=True),
        value_type=TunableTuple(
            description=
            '\n            Tying each state to a text string and a value which can be called\n            by UI.\n            ',
            text_value=TunableLocalizedString(
                description=
                '\n                Localization String For the name of the collection.  \n                This will be read on the collection UI to show item rarities.\n                '
            ),
            rarity_value=TunableEnumEntry(
                description=
                '\n                Rarity enum called for UI to determine sorting in the\n                collection UI\n                ',
                tunable_type=ObjectCollectionRarity,
                needs_tuning=True,
                default=ObjectCollectionRarity.COMMON,
                binary_type=EnumBinaryExportType.EnumUint32),
            export_class_name='CollectionRarity'),
        tuple_name='CollectionRarityMapping',
        export_modes=ExportModes.ClientBinary)
    COLLECTION_COLLECTED_STING = TunablePlayAudio(
        description=
        '\n            The audio sting that gets played when a collectible is found.\n            '
    )
    COLLECTION_COMPLETED_STING = TunablePlayAudio(
        description=
        '\n            The audio sting that gets played when a collection is completed.\n            '
    )
    COLLECTED_INVALID_STATES = TunableList(
        description=
        '\n            List of states the collection system will check for in an object.\n            If the object has any of these states the collectible will not\n            be counted.\n            Example: Unidentified states on herbalism.\n            ',
        tunable=TunableReference(
            description=
            '\n                The state value the object will have to invalidate its \n                collected event.\n                ',
            manager=services.get_instance_manager(
                sims4.resources.Types.OBJECT_STATE),
            pack_safe=True))
    COLLECTED_RARITY_STATE = TunableReference(
        description=
        '\n            The rarity state the collection system will use for an object.\n            The object will need this state to call the rarity state/text.\n            ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE))
    _COLLECTION_DATA = {}
    _BONUS_COLLECTION_DATA = {}

    @classmethod
    def initialize_collection_data(cls):
        if not cls._COLLECTION_DATA:
            for collection_data in cls.COLLECTIONS_DEFINITION:
                for collectible_object in collection_data.object_list:
                    collectible_object._collection_id = collection_data.collection_id
                    cls._COLLECTION_DATA[collectible_object.collectable_item.
                                         id] = collectible_object
                for collectible_object in collection_data.bonus_object_list:
                    collectible_object._collection_id = collection_data.collection_id
                    cls._BONUS_COLLECTION_DATA[
                        collectible_object.collectable_item.
                        id] = collectible_object

    @classmethod
    def get_collection_info_by_definition(cls, obj_def_id):
        if not cls._COLLECTION_DATA:
            ObjectCollectionData.initialize_collection_data()
        collectible = cls._COLLECTION_DATA.get(obj_def_id)
        if collectible:
            return (collectible._collection_id, collectible, True)
        else:
            collectible = cls._BONUS_COLLECTION_DATA.get(obj_def_id)
            if collectible:
                return (collectible._collection_id, collectible, False)
        return (None, None, None)

    @classmethod
    def is_base_object_of_collection(cls, obj_def_id, collection_id):
        if not cls._COLLECTION_DATA:
            ObjectCollectionData.initialize_collection_data()
        return obj_def_id in cls._COLLECTION_DATA

    @classmethod
    def get_collection_data(cls, collection_id):
        for collection_data in cls.COLLECTIONS_DEFINITION:
            if collection_data.collection_id == collection_id:
                return collection_data
예제 #13
0
class Narrative(HasTunableReference,
                metaclass=HashedTunedInstanceMetaclass,
                manager=services.get_instance_manager(Types.NARRATIVE)):
    INSTANCE_TUNABLES = {
        'narrative_groups':
        TunableEnumSet(
            description=
            '\n            A set of narrative groups this narrative is a member of.\n            ',
            enum_type=NarrativeGroup,
            enum_default=NarrativeGroup.INVALID,
            invalid_enums=(NarrativeGroup.INVALID, )),
        'narrative_links':
        TunableMapping(
            description=
            '\n            A mapping of narrative event to the narrative that will trigger \n            when that narrative event triggers.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                Event of interest.\n                ',
                tunable_type=NarrativeEvent,
                default=NarrativeEvent.INVALID,
                invalid_enums=(NarrativeEvent.INVALID, )),
            value_type=TunableReference(
                description=
                '\n                The narrative the respective event transitions to while\n                this specific narrative is active. \n                ',
                manager=services.get_instance_manager(Types.NARRATIVE))),
        'additional_situation_shifts':
        TunableMapping(
            description=
            '\n            A mapping of situation shift type to the shift curve it provides.\n            ',
            key_type=TunableEnumEntry(
                description='\n                Shift type.\n                ',
                tunable_type=NarrativeSituationShiftType,
                default=NarrativeSituationShiftType.INVALID,
                invalid_enums=(NarrativeSituationShiftType.INVALID, )),
            value_type=SituationCurve.TunableFactory(
                description=
                '\n                The situation schedule this adds to the situation scheduler\n                if this shift type is opted into as an additional source.\n                ',
                get_create_params={'user_facing': False})),
        'situation_replacements':
        TunableMapping(
            description=
            '\n            A mapping of situation to a tuple of situation and tests to apply.\n            ',
            key_type=TunableReference(
                description=
                '\n                A situation that is available for situation replacement.\n                ',
                manager=services.get_instance_manager(Types.SITUATION)),
            value_type=TunableTuple(replacement=TunableReference(
                description=
                '\n                    A situation that is available for situation replacement.\n                    ',
                manager=services.get_instance_manager(Types.SITUATION)),
                                    replacement_tests=
                                    SituationReplacementTestList())),
        'environment_override':
        OptionalTunable(
            description=
            '\n            If tuned, this narrative can have some effect on world controls\n            such as skyboxes, ambient sounds, and vfx.\n            ',
            tunable=NarrativeEnvironmentOverride.TunableFactory()),
        'introduction':
        OptionalTunable(
            description=
            '\n            If enabled, an introduction dialog will be shown on the next zone\n            load (which could be a save/load, travel, switch to another\n            household, etc.) if the test passes.\n            ',
            tunable=TunableTuple(
                dialog=UiDialogOk.TunableFactory(
                    description=
                    '\n                    The dialog to show that introduces the narrative.\n                    '
                ),
                tests=TunableTestSet(
                    description=
                    '\n                    The test set that must pass for the introduction to be\n                    given. Only the global resolver is available.\n                    Sample use: Must be in a specific region.\n                    '
                ))),
        'dialog_on_activation':
        OptionalTunable(
            description=
            '\n            If enabled, an introduction dialog will be shown when the narrative\n            is activated, if the test passes.\n            ',
            tunable=TunableTuple(
                dialog=TunableUiDialogVariant(
                    description=
                    '\n                    The dialog to show when the narrative starts.\n                    '
                ),
                tests=TunableTestSet(
                    description=
                    '\n                    The test set that must pass for the dialog to be\n                    given. Only the global resolver is available.\n                    Sample use: Must be in a specific region.\n                    '
                ))),
        'audio_sting':
        OptionalTunable(
            description=
            '\n            If enabled, play the specified audio sting when this narrative starts.\n            ',
            tunable=TunablePlayAudio()),
        'sim_info_loots':
        OptionalTunable(
            description=
            '\n            Loots that will be given to all sim_infos when this narrative starts.\n            ',
            tunable=TunableTuple(
                loots=TunableList(tunable=TunableReference(
                    manager=services.get_instance_manager(
                        sims4.resources.Types.ACTION),
                    class_restrictions=('LootActions', ),
                    pack_safe=True)),
                save_lock_tooltip=TunableLocalizedString(
                    description=
                    '\n                    The tooltip/message to show on the save lock tooltip while\n                    the loots are processing.\n                    '
                ))),
        'narrative_threshold_links':
        TunableMapping(
            description=
            "\n            A mapping between the event listener to a narrative link\n            that will be activated if progress of that event type hits \n            the tuned threshold.          \n            \n            For example, if this narrative has the following narrative threshold\n            link:\n            \n            {\n            key type: GoldilocksListener\n            value_type:\n              Interval: -10, 10\n              below_link: TooCold_Goldilocks\n              above_link: TooHot_Goldilocks\n            }\n            \n            ... any Narrative Progression Loot tagged with the GoldilocksListener\n            event will increment this instance's narrative_progression_value. If\n            it ever goes above 10 or below -10, the corresponding narrative is\n            activated and this narrative will complete.\n            \n            NOTE: All active narratives' progression values begin at 0.   \n            ",
            key_type=TunableEnumEntry(
                description=
                '\n                The progression event that triggers the narrative transition\n                if a threshold is met.\n                ',
                tunable_type=NarrativeProgressionEvent,
                default=NarrativeProgressionEvent.INVALID,
                invalid_enums=(NarrativeProgressionEvent.INVALID, )),
            value_type=TunableTuple(
                interval=TunableInterval(
                    description=
                    '\n                    The interval defines the upper and lower bound of the\n                    narrative thresholds. If any of the thresholds are crossed,\n                    the corresponding narrative is activated.\n                    ',
                    tunable_type=int,
                    default_lower=-50,
                    default_upper=50),
                below_link=OptionalTunable(
                    description=
                    '\n                    The narrative that is activated if the lower threshold is\n                    passed.\n                    ',
                    tunable=TunableReference(
                        manager=services.get_instance_manager(
                            Types.NARRATIVE))),
                above_link=OptionalTunable(
                    description=
                    '\n                    The narrative that is activated if the upper threshold is\n                    passed.\n                    ',
                    tunable=TunableReference(
                        manager=services.get_instance_manager(
                            Types.NARRATIVE)))))
    }

    def __init__(self):
        self._introduction_shown = False
        self._should_suppress_travel_sting = False
        self._narrative_progression = {}
        for event in self.narrative_threshold_links.keys():
            self._narrative_progression[event] = 0

    def save(self, msg):
        msg.narrative_id = self.guid64
        msg.introduction_shown = self._introduction_shown
        for (event, progression) in self._narrative_progression.items():
            with ProtocolBufferRollback(
                    msg.narrative_progression_entries) as progression_msg:
                progression_msg.event = event
                progression_msg.progression = progression

    def load(self, msg):
        self._introduction_shown = msg.introduction_shown
        for narrative_progression_data in msg.narrative_progression_entries:
            self._narrative_progression[
                narrative_progression_data.
                event] = narrative_progression_data.progression

    def on_zone_load(self):
        self._should_suppress_travel_sting = False
        if self.introduction is not None:
            if not self._introduction_shown:
                resolver = GlobalResolver()
                if self.introduction.tests.run_tests(resolver):
                    dialog = self.introduction.dialog(None, resolver=resolver)
                    dialog.show_dialog()
                    self._introduction_shown = True
                    self._should_suppress_travel_sting = self.introduction.dialog.audio_sting is not None

    @property
    def should_suppress_travel_sting(self):
        return self._should_suppress_travel_sting

    def start(self):
        if self.dialog_on_activation is not None:
            resolver = GlobalResolver()
            if self.dialog_on_activation.tests.run_tests(resolver):
                dialog = self.dialog_on_activation.dialog(None,
                                                          resolver=resolver)
                dialog.show_dialog()
        if self.audio_sting is not None:
            play_tunable_audio(self.audio_sting)
        if self.sim_info_loots is not None:
            services.narrative_service().add_sliced_sim_info_loots(
                self.sim_info_loots.loots,
                self.sim_info_loots.save_lock_tooltip)

    def apply_progression_for_event(self, event, amount):
        if event not in self.narrative_threshold_links:
            return ()
        self._narrative_progression[event] += amount
        new_amount = self._narrative_progression[event]
        link_data = self.narrative_threshold_links[event]
        if new_amount in link_data.interval:
            return ()
        if new_amount < link_data.interval.lower_bound and link_data.below_link is not None:
            return (link_data.below_link, )
        elif link_data.above_link is not None:
            return (link_data.above_link, )
        return ()

    def get_progression_stat(self, event):
        return self._narrative_progression.get(event)