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))
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
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')
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)
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)}
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)
class Gig(HasTunableReference, _GigDisplayMixin, PrepTaskTrackerMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.CAREER_GIG)): INSTANCE_TUNABLES = { 'career': TunableReference( description= '\n The career this gig is associated with.\n ', manager=services.get_instance_manager( sims4.resources.Types.CAREER)), 'gig_time': WeeklySchedule.TunableFactory( description= '\n A tunable schedule that will determine when you have to be at work.\n ', export_modes=ExportModes.All), 'gig_prep_time': TunableTimeSpan( description= '\n The amount of time between when a gig is selected and when it\n occurs.\n ', default_hours=5), 'gig_prep_tasks': TunableList( description= '\n A list of prep tasks the Sim can do to improve their performance\n during the gig. \n ', tunable=PrepTask.TunableFactory()), 'loots_on_schedule': TunableList( description= '\n Loot actions to apply when a sim gets a gig.\n ', tunable=LootActions.TunableReference()), 'audio_on_prep_task_completion': OptionalTunable( description= '\n A sting to play at the time a prep task completes.\n ', tunable=TunablePlayAudio( locked_args={ 'immediate_audio': True, 'joint_name_hash': None, 'play_on_active_sim_only': True })), 'gig_pay': TunableVariant( description= '\n Base amount of pay for this gig. Can be either a flat amount or a\n range.\n ', range=TunableInterval(tunable_type=int, default_lower=0, default_upper=100, minimum=0), flat_amount=TunableIntervalLiteral(tunable_type=int, default=0, minimum=0), default='range'), 'additional_pay_per_overmax_level': OptionalTunable( description= '\n If checked, overmax levels will be considered when calculating pay\n for this gig. The actual implementation of this may vary by gig\n type.\n ', tunable=TunableRange(tunable_type=int, default=0, minimum=0)), 'result_based_gig_pay_multipliers': OptionalTunable( description= '\n A set of multipliers for gig pay. The multiplier used depends on the\n GigResult of the gig. The meanings of each GigResult may vary by\n gig type.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the additional pay\n the sim will receive.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory())), 'initial_result_based_career_performance': OptionalTunable( description= "\n A mapping between the GigResult for this gig and the initial\n career performance for the Sim's first gig.\n ", tunable=TunableMapping( description= '\n A map between the result type of the gig and the initial career\n performance the Sim will receive.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=Tunable( description= '\n The initial performance value that will be applied.\n ', tunable_type=float, default=0))), 'result_based_career_performance': OptionalTunable( description= '\n A mapping between the GigResult for this gig and the change in\n career performance for the sim.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the career\n performance the sim will receive.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=Tunable( description= '\n The performance modification.\n ', tunable_type=float, default=0))), 'result_based_career_performance_multiplier': OptionalTunable( description= '\n A mapping between the GigResult and the multiplier for the career \n performance awarded.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the career\n performance multiplier.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The performance modification multiplier.\n ' ))), 'result_based_loots': OptionalTunable( description= '\n A mapping between the GigResult for this gig and a loot list to\n optionally apply. The resolver for this loot list is either a\n SingleSimResolver of the working sim or a DoubleSimResolver with the\n target being the customer if there is a customer sim.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the loot list.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableList( description= '\n Loot actions to apply.\n ', tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)))), 'payout_stat_data': TunableMapping( description= '\n Stats, and its associated information, that are gained (or lost) \n when sim finishes this gig.\n ', key_type=TunableReference( description= '\n Stat for this payout.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), value_type=TunableTuple( description= '\n Data about this payout stat. \n ', base_amount=Tunable( description= '\n Base amount (pre-modifiers) applied to the sim at the end\n of the gig.\n ', tunable_type=float, default=0.0), medal_to_payout=TunableMapping( description= '\n Mapping of medal -> stat multiplier.\n ', key_type=TunableEnumEntry( description= '\n Medal achieved in this gig.\n ', tunable_type=SituationMedal, default=SituationMedal.TIN), value_type=TunableMultiplier.TunableFactory( description= '\n Mulitiplier on statistic payout if scorable situation\n ends with the associate medal.\n ' )), ui_threshold=TunableList( description= '\n Thresholds and icons we use for this stat to display in \n the end of day dialog. Tune in reverse of highest threshold \n to lowest threshold.\n ', tunable=TunableTuple( description= '\n Threshold and icon for this stat and this gig.\n ', threshold_icon=TunableIcon( description= '\n Icon if the stat is of this threshold.\n ' ), threshold_description=TunableLocalizedStringFactory( description= '\n Description to use with icon\n ' ), threshold=Tunable( description= '\n Threshold that the stat must >= to.\n ', tunable_type=float, default=0.0))))), 'career_events': TunableList( description= '\n A list of available career events for this gig.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.CAREER_EVENT))), 'gig_cast_rel_bit_collection_id': TunableEnumEntry( description= '\n If a rel bit is applied to the cast member, it must be of this collection id.\n We use this to clear the rel bit when the gig is over.\n ', tunable_type=RelationshipBitCollectionUid, default=RelationshipBitCollectionUid.Invalid, invalid_enums=(RelationshipBitCollectionUid.All, )), 'gig_cast': TunableList( description= '\n This is the list of sims that need to spawn for this gig. \n ', tunable=TunableTuple( description= '\n Data for cast members. It contains a test which tests against \n the owner of this gig and spawn the necessary sims. A bit\n may be applied through the loot action to determine the type \n of cast members (costars, directors, etc...) \n ', filter_test=TunableTestSet( description= '\n Test used on owner sim.\n ' ), sim_filter=TunableSimFilter.TunableReference( description= '\n If filter test is passed, this sim is created and stored.\n ' ), cast_member_rel_bit=OptionalTunable( description= '\n If tuned, this rel bit will be applied on the spawned cast \n member.\n ', tunable=RelationshipBit.TunableReference( description= '\n Rel bit to apply.\n ' )))), 'end_of_gig_dialog': OptionalTunable( description= '\n A results dialog to show. This dialog allows a list\n of icons with labels. Stats are added at the end of this icons.\n ', tunable=UiDialogLabeledIcons.TunableFactory()), 'disabled_tooltip': OptionalTunable( description= '\n If tuned, the tooltip when this row is disabled.\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'end_of_gig_notifications': OptionalTunable( description= '\n If enabled, a notification to show at the end of the gig instead of\n a normal career message. Tokens are:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The pay for the gig\n * 5: The gratuity for the gig\n * 6: The customer (sim) of the gig, if there is a customer.\n * 7: A bullet list of loots and payments as a result of this gig.\n This list uses the text tuned on the loots themselves to create\n bullets for each loot. Those texts will generally have tokens 0\n and 1 be the subject and target sims (of the loot) but may\n have additional tokens depending on the type of loot.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the post-gig\n notification.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=_get_career_notification_tunable_factory()), tuning_group=GroupNames.UI), 'end_of_gig_overmax_notification': OptionalTunable( description= '\n If tuned, the notification that will be used if the sim gains an\n overmax level during this gig. Will override the overmax\n notification in career messages. The following tokens are provided:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The overmax level\n * 5: The pay for the gig\n * 6: Additional pay tuned at additional_pay_per_overmax_level \n * 7: The overmax rewards in a bullet-point list, in the form of a\n string. These are tuned on the career_track\n ', tunable=_get_career_notification_tunable_factory(), tuning_group=GroupNames.UI), 'end_of_gig_overmax_rewardless_notification': OptionalTunable( description= '\n If tuned, the notification that will be used if the sim gains an\n overmax level with no reward during this gig. Will override the\n overmax rewardless notification in career messages.The following\n tokens are provided:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The overmax level\n * 5: The pay for the gig\n * 6: Additional pay tuned at additional_pay_per_overmax_level \n ', tunable=_get_career_notification_tunable_factory(), tuning_group=GroupNames.UI), 'end_of_gig_promotion_text': OptionalTunable( description= '\n A string that, if enabled, will be pre-pended to the bullet\n list of results in the promotion notification. Tokens are:\n * 0 : The Sim owner of the career\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'end_of_gig_demotion_text': OptionalTunable( description= '\n A string that, if enabled, will be pre-pended to the bullet\n list of results in the promotion notification. Tokens are:\n * 0 : The Sim owner of the career\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'odd_job_tuning': OptionalTunable( description= '\n Tuning specific to odd jobs. Leave untuned if this gig is not an\n odd job.\n ', tunable=TunableTuple( customer_description=TunableLocalizedStringFactory( description= '\n The description of the odd job written by the customer.\n Token 0 is the customer sim.\n ' ), use_customer_description_as_gig_description=Tunable( description= '\n If checked, the customer description will be used as the\n gig description. This description is used as the tooltip\n for the gig icon in the career panel.\n ', tunable_type=bool, default=False), result_based_gig_gratuity_multipliers=TunableMapping( description= '\n A set of multipliers for the gig gratuity. This maps the\n result type of the gig and the gratuity multiplier (a \n percentage). The base pay will be multiplied by this \n multiplier in order to determine the actual gratuity \n amount.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the \n Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The gratuity multiplier to be calculated for this \n GigResult.\n ' )), result_based_gig_gratuity_chance_multipliers=TunableMapping( description= '\n A set of multipliers for determining the gig gratuity \n chance (i.e., the probability the Sim will receive gratuity \n in addition to the base pay). The gratuity chance depends \n on the GigResult of the gig. This maps the result type of \n the gig and the gratuity chance/percentage. If this map\n (or a GigResult) is left untuned, then no gratuity is \n added.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the \n Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The multiplier to be calculated for this GigResult. \n This represents the percentage chance the Sim will \n receive gratuity. If the Sim is to not receive \n gratuity, the base value should be 0 (without further\n tests). If this Sim is guaranteed to receive gratuity,\n the base value should be 1 (without further tests).\n ' )), gig_gratuity_bullet_point_text=OptionalTunable( description= '\n If enabled, the gig gratuity will be a bullet point in the\n bullet pointed list of loots and money supplied to the end\n of gig notification (this is token 7 of that notification).\n If disabled, gratuity will be omitted from that list.\n Tokens:\n * 0: The sim owner of the career\n * 1: The customer\n * 2: the gratuity amount \n ', tunable=TunableLocalizedStringFactory()))), 'tip': OptionalTunable( description= '\n A tip that is displayed with the gig in pickers and in the career\n panel. Can produce something like "Required Skill: Fitness 2".\n ', tunable=TunableTuple( tip_title=TunableLocalizedStringFactory( description= '\n The title string of the tip. Could be something like "Required\n Skill.\n ' ), tip_text=TunableLocalizedStringFactory( description= '\n The text string of the tip. Could be something like "Fitness 2".\n ' ), tip_icon=OptionalTunable(tunable=TunableIcon( description= '\n An icon to show along with the tip.\n ' )))), 'critical_failure_test': OptionalTunable( description= '\n The tests for checking whether or not the Sim should receive the \n CRITICAL_FAILURE outcome. This will override other GigResult \n behavior.\n ', tunable=TunableTestSet( description= '\n The tests to be performed on the Sim (and any customer). If \n the tests pass, the outcome will be CRITICAL_FAILURE. \n ' )) } def __init__(self, owner, customer=None, *args, **kwargs): super().__init__(*args, **kwargs) self._owner = owner self._customer_id = customer.id if customer is not None else None self._upcoming_gig_time = None self._gig_result = None self._gig_pay = None self._gig_gratuity = None self._loot_strings = None self._gig_attended = False @classmethod def get_aspiration(cls): pass @classmethod def get_time_until_next_possible_gig(cls, starting_time): required_prep_time = cls.gig_prep_time() start_considering_prep = starting_time + required_prep_time (time_until, _) = cls.gig_time().time_until_next_scheduled_event( start_considering_prep) if not time_until: return return time_until + required_prep_time def register_aspiration_callbacks(self): aspiration = self.get_aspiration() if aspiration is None: return aspiration.register_callbacks() aspiration_tracker = self._owner.aspiration_tracker if aspiration_tracker is None: return aspiration_tracker.validate_and_return_completed_status(aspiration) aspiration_tracker.process_test_events_for_aspiration(aspiration) def notify_gig_attended(self): self._gig_attended = True def has_attended_gig(self): return self._gig_attended def notify_canceled(self): self._gig_result = GigResult.CANCELED self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_CANCEL) def get_career_performance(self, first_gig=False): if not self.result_based_career_performance: return 0 if self.initial_result_based_career_performance is not None and first_gig and self._gig_result in self.initial_result_based_career_performance: return self.initial_result_based_career_performance[ self._gig_result] performance_modifier = 1 if self.result_based_career_performance_multiplier: if self._gig_result in self.result_based_career_performance_multiplier: resolver = self.get_resolver_for_gig() performance_modifier = self.result_based_career_performance_multiplier[ self._gig_result].get_multiplier(resolver) return self.result_based_career_performance.get( self._gig_result, 0) * performance_modifier def treat_work_time_as_due_date(self): return False @classmethod def create_picker_row(cls, description=None, scheduled_time=None, owner=None, gig_customer=None, enabled=True, **kwargs): tip = cls.tip description = cls.gig_picker_localization_format( cls.gig_pay.lower_bound, cls.gig_pay.upper_bound, scheduled_time, tip.tip_title(), tip.tip_text(), gig_customer) if not enabled and cls.disabled_tooltip is not None: row_tooltip = lambda *_: cls.disabled_tooltip(owner) elif cls.display_description is None: row_tooltip = None else: row_tooltip = lambda *_: cls.display_description(owner) if cls.odd_job_tuning is not None: customer_description = cls.odd_job_tuning.customer_description( gig_customer) row = OddJobPickerRow(customer_id=gig_customer.id, customer_description=customer_description, tip_title=tip.tip_title(), tip_text=tip.tip_text(), tip_icon=tip.tip_icon, name=cls.display_name(owner), icon=cls.display_icon, row_description=description, row_tooltip=row_tooltip, is_enable=enabled) else: row = ObjectPickerRow(name=cls.display_name(owner), icon=cls.display_icon, row_description=description, row_tooltip=row_tooltip, is_enable=enabled) return row def get_gig_time(self): return self._upcoming_gig_time def get_gig_customer(self): return self._customer_id def clean_up_gig(self): if self.gig_prep_tasks: self.prep_task_cleanup() def save_gig(self, gig_proto_buff): gig_proto_buff.gig_type = self.guid64 gig_proto_buff.gig_time = self._upcoming_gig_time if hasattr(gig_proto_buff, 'gig_attended'): gig_proto_buff.gig_attended = self._gig_attended if self._customer_id: gig_proto_buff.customer_sim_id = self._customer_id def load_gig(self, gig_proto_buff): self._upcoming_gig_time = DateAndTime(gig_proto_buff.gig_time) if self.gig_prep_tasks: self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64, self.audio_on_prep_task_completion, from_load=True) if gig_proto_buff.HasField('customer_sim_id'): self._customer_id = gig_proto_buff.customer_sim_id if gig_proto_buff.HasField('gig_attended'): self._gig_attended = gig_proto_buff.gig_attended def set_gig_time(self, upcoming_gig_time): self._upcoming_gig_time = upcoming_gig_time def get_resolver_for_gig(self): if self._customer_id is not None: customer_sim_info = services.sim_info_manager().get( self._customer_id) if customer_sim_info is not None: return DoubleSimResolver(self._owner, customer_sim_info) return SingleSimResolver(self._owner) def set_up_gig(self): if self.gig_prep_tasks: self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64, self.audio_on_prep_task_completion) if self.loots_on_schedule: resolver = self.get_resolver_for_gig() for loot_actions in self.loots_on_schedule: loot_actions.apply_to_resolver(resolver) self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_STARTED) def collect_rabbit_hole_rewards(self): pass def _get_additional_loots(self): if self.result_based_loots is not None and self._gig_result is not None: loots = self.result_based_loots.get(self._gig_result) if loots is not None: return loots return () def collect_additional_rewards(self): loots = self._get_additional_loots() if loots: self._loot_strings = [] resolver = self.get_resolver_for_gig() for loot_actions in loots: self._loot_strings.extend( loot_actions.apply_to_resolver_and_get_display_texts( resolver)) def _determine_gig_outcome(self): raise NotImplementedError def get_pay(self, overmax_level=None, **kwargs): self._determine_gig_outcome() pay = self.gig_pay.lower_bound if self.additional_pay_per_overmax_level: pay = pay + overmax_level * self.additional_pay_per_overmax_level resolver = self.get_resolver_for_gig() if self.result_based_gig_pay_multipliers: if self._gig_result in self.result_based_gig_pay_multipliers: multiplier = self.result_based_gig_pay_multipliers[ self._gig_result].get_multiplier(resolver) pay = int(pay * multiplier) gratuity = 0 if self.odd_job_tuning: if self.odd_job_tuning.result_based_gig_gratuity_multipliers: if self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers: gratuity_multiplier = 0 gratuity_chance = 0 if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers: gratuity_chance = self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers[ self._gig_result].get_multiplier(resolver) if random.random() <= gratuity_chance: if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_multipliers: gratuity_multiplier = self.odd_job_tuning.result_based_gig_gratuity_multipliers[ self._gig_result].get_multiplier(resolver) gratuity = int(pay * gratuity_multiplier) self._gig_pay = pay self._gig_gratuity = gratuity return pay + gratuity def get_promotion_evaluation_result(self, reward_text, *args, first_gig=False, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) if self.end_of_gig_promotion_text and not first_gig: results_list = self.get_results_list( self.end_of_gig_promotion_text(self._owner)) else: results_list = self.get_results_list() return EvaluationResult(Evaluation.PROMOTED, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, results_list) def get_demotion_evaluation_result(self, *args, first_gig=False, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) if self.end_of_gig_demotion_text and not first_gig: results_list = self.get_results_list( self.end_of_gig_demotion_text(self._owner)) else: results_list = self.get_results_list() return EvaluationResult(Evaluation.DEMOTED, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, results_list) def get_overmax_evaluation_result(self, overmax_level, reward_text, *args, **kwargs): if reward_text and self.end_of_gig_overmax_notification: return EvaluationResult(Evaluation.PROMOTED, self.end_of_gig_overmax_notification, overmax_level, self._gig_pay, self.additional_pay_per_overmax_level, reward_text) elif self.end_of_gig_overmax_rewardless_notification: return EvaluationResult( Evaluation.PROMOTED, self.end_of_gig_overmax_rewardless_notification, overmax_level, self._gig_pay, self.additional_pay_per_overmax_level) def _get_strings_for_results_list(self): strings = [] if self._gig_pay is not None: strings.append(LocalizationHelperTuning.MONEY(self._gig_pay)) if self.odd_job_tuning is not None and self._gig_gratuity: gratuity_text_factory = self.odd_job_tuning.gig_gratuity_bullet_point_text if gratuity_text_factory is not None: customer_sim_info = services.sim_info_manager().get( self._customer_id) gratuity_text = gratuity_text_factory(self._owner, customer_sim_info, self._gig_gratuity) strings.append(gratuity_text) if self._loot_strings: strings.extend(self._loot_strings) return strings def get_results_list(self, *additional_tokens): return LocalizationHelperTuning.get_bulleted_list( None, *additional_tokens, *self._get_strings_for_results_list()) def get_end_of_gig_evaluation_result(self, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) return EvaluationResult(Evaluation.ON_TARGET, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, self.get_results_list()) @classmethod def _get_base_pay_for_gig_owner(cls, owner): overmax_pay = cls.additional_pay_per_overmax_level if overmax_pay is not None: career = owner.career_tracker.get_career_by_uid(cls.career.guid64) if career is not None: overmax_pay *= career.overmax_level return (cls.gig_pay.lower_bound + overmax_pay, cls.gig_pay.upper_bound + overmax_pay) return (cls.gig_pay.lower_bound, cls.gig_pay.upper_bound) @classmethod def build_gig_msg(cls, msg, sim, gig_time=None, gig_customer=None): msg.gig_type = cls.guid64 msg.gig_name = cls.display_name(sim) (pay_lower, pay_upper) = cls._get_base_pay_for_gig_owner(sim) msg.min_pay = pay_lower msg.max_pay = pay_upper msg.gig_icon = ResourceKey() msg.gig_icon.instance = cls.display_icon.instance msg.gig_icon.group = cls.display_icon.group msg.gig_icon.type = cls.display_icon.type if cls.odd_job_tuning is not None and cls.odd_job_tuning.use_customer_description_as_gig_description and gig_customer is not None: customer_sim_info = services.sim_info_manager().get(gig_customer) if customer_sim_info is not None: msg.gig_description = cls.odd_job_tuning.customer_description( customer_sim_info) else: msg.gig_description = cls.display_description(sim) if gig_time is not None: msg.gig_time = gig_time if gig_customer is not None: msg.customer_id = gig_customer if cls.tip is not None: msg.tip_title = cls.tip.tip_title() if cls.tip.tip_icon is not None or cls.tip.tip_text is not None: build_icon_info_msg( IconInfoData(icon_resource=cls.tip.tip_icon), None, msg.tip_icon, desc=cls.tip.tip_text()) def send_prep_task_update(self): if self.gig_prep_tasks: self._prep_task_tracker.send_prep_task_update() def _apply_payout_stat(self, medal, payout_display_data=None): owner_sim = self._owner resolver = SingleSimResolver(owner_sim) payout_stats = self.payout_stat_data for stat in payout_stats.keys(): stat_tracker = owner_sim.get_tracker(stat) if not owner_sim.get_tracker(stat).has_statistic(stat): continue stat_data = payout_stats[stat] stat_multiplier = 1.0 if medal in stat_data.medal_to_payout: multiplier = stat_data.medal_to_payout[medal] stat_multiplier = multiplier.get_multiplier(resolver) stat_total = stat_data.base_amount * stat_multiplier stat_tracker.add_value(stat, stat_total) if payout_display_data is not None: for threshold_data in stat_data.ui_threshold: if stat_total >= threshold_data.threshold: payout_display_data.append(threshold_data) break def _send_gig_telemetry(self, progress): with telemetry_helper.begin_hook(gig_telemetry_writer, TELEMETRY_HOOK_GIG_PROGRESS, sim_info=self._owner) as hook: hook.write_int(TELEMETRY_CAREER_ID, self.career.guid64) hook.write_int(TELEMETRY_GIG_ID, self.guid64) hook.write_int(TELEMETRY_GIG_PROGRESS_NUMBER, progress)
class 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)
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()
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
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
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)