class ZoneModifierDisplayInfo(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( Types.USER_INTERFACE_INFO)): base_game_only = True INSTANCE_TUNABLES = { 'zone_modifier_icon': TunableIcon( description="\n The zone modifier's icon.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'zone_modifier_name': TunableLocalizedString( description="\n The zone modifier's name.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'zone_modifier_description': TunableLocalizedString( description= "\n The zone modifier's description.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'zone_modifier_reference': TunablePackSafeReference( description= '\n The zone modifier gameplay tuning reference ID.\n \n This ID will be what is persisted in save data and used\n for any lookups.\n ', manager=services.zone_modifier_manager(), export_modes=ExportModes.All, tuning_group=GroupNames.UI) }
def __init__(self, description='', **kwargs): super().__init__( default_icon=TunableIcon( description= '\n Icon to display for this menu item.\n ' ), selected_icon=TunableIcon( description= '\n Icon to use for the selected state of the item.\n If not specified, will fall back to the default icon.\n ', allow_none=True), new_content_icon=TunableIcon( description= '\n Icon to use when there is new content related to this item.\n If not specified, will fall back to the default icon.\n ', allow_none=True), description=description, **kwargs)
def __init__(self, *args, **kwargs): super().__init__( *args, bg_image=TunableIcon( description= '\n Image resource to display as UI phone panel background.\n ' ), icon=TunableIcon( description= '\n Icon to display for phone color selector swatch.\n ' ), phone_trait=TunableReference( description= '\n Trait associated with cell phone color.\n ', allow_none=True, manager=services.get_instance_manager( sims4.resources.Types.TRAIT)), **kwargs)
class UiDialogLabeledIcons(UiDialogOk): FACTORY_TUNABLES = { 'labeled_icons': TunableList( TunableTuple( description= '\n A list of icons and labels to display in the UI dialog.\n ', icon=TunableIcon(), label=TunableLocalizedStringFactory())) } def build_msg(self, additional_tokens=(), additional_icons=None, **kwargs): msg = super().build_msg(additional_tokens=additional_tokens, **kwargs) msg.dialog_type = Dialog_pb2.UiDialogMessage.ICONS_LABELS for labeled_icon in self.labeled_icons: msg.icon_infos.append( create_icon_info_msg(IconInfoData(labeled_icon.icon), name=self._build_localized_string_msg( labeled_icon.label, *additional_tokens))) if additional_icons: msg.icon_infos.extend(additional_icons) return msg
class UniversityCourseData(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_COURSE_DATA)): INSTANCE_TUNABLES = {'spawn_point_tag': TunableMapping(description='\n University specific spawn point tags.\n Used by course related interactions to determine which spawn\n point to use for the constraint. (i.e. the one in front of the\n appropriate building)\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableSet(tunable=TunableEnumWithFilter(tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('Spawn',)), minlength=1)), 'classroom_tag': TunableMapping(description='\n University specific classroom tags.\n Used by university interactions on shells to determine which building\n shell should have the interaction(s) available.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableSet(tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID), minlength=1)), 'university_course_mapping': TunableMapping(description='\n University specific course name and description.\n Each university can have its own course name and description\n defined.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableTuple(course_name=TunableLocalizedStringFactory(description='\n The name of this course.\n '), course_description=TunableLocalizedString(description='\n A description for this course.\n ', allow_none=True), export_class_name='UniversityCourseDisplayData'), tuple_name='UniversityCourseDataMapping', export_modes=ExportModes.All), 'course_skill_data': TunableTuple(description='\n The related skill data for this specific course. Whenever a Sim \n does something that increases their course grade performance (like\n attending lecture or studying), this skill will also increase by\n the tunable amount. Likewise, whenever this related skill \n increases, the course grade will also increase.\n ', related_skill=OptionalTunable(description='\n The related skill associated with this course.\n ', tunable=TunablePackSafeReference(manager=services.get_instance_manager(Types.STATISTIC), class_restrictions=('Skill',)))), 'icon': TunableIcon(description='\n Icon for this university course.\n ', export_modes=ExportModes.All, allow_none=True), 'cost': TunableRange(description='\n The cost of this course.\n ', tunable_type=int, default=200, minimum=0, export_modes=ExportModes.All), 'course_tags': TunableTags(description='\n The tag for this course. Used for objects that may be shared \n between courses.\n ', filter_prefixes=['course']), 'final_requirement_type': TunableEnumEntry(description='\n The final requirement for this course. This requirement must be \n completed before the course can be considered complete.\n ', tunable_type=FinalCourseRequirement, default=FinalCourseRequirement.NONE), 'final_requirement_aspiration': TunableReference(description='\n An aspiration to use for tracking the final course requirement. \n ', manager=services.get_instance_manager(sims4.resources.Types.ASPIRATION), class_restrictions='AspirationAssignment', allow_none=True), 'professor_assignment_trait': TunableMapping(description='\n A mapping of University -> professor assignment trait.\n \n This is needed because each of the universities shipped with EP08\n use the exact same classes but we want different teachers for each\n university.\n ', key_type=TunableReference(description='\n A reference to the University that the professor will belong to.\n ', manager=services.get_instance_manager(sims4.resources.Types.UNIVERSITY)), value_type=TunableReference(description='\n The trait used to identify the professor for this course.\n ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT)))} @classproperty def is_elective(cls): return any(cls is e.elective for e in University.COURSE_ELECTIVES.electives)
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 Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)): SKILL_LEVEL_LIST = TunableMapping(description='\n A mapping defining the level boundaries for each skill type.\n ', key_type=SkillLevelType, value_type=TunableList(description='\n The level boundaries for skill type, specified as a delta from the\n previous value.\n ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All) SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n Skill gain points based on skill effectiveness.\n ', key_type=SkillEffectiveness, value_type=TunableCurve()) DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n Interval used when dynamic loot is used in a\n PeriodicStatisticChangeElement.\n ', tunable_type=float, default=1, minimum=1) INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n The name of this skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n The skill's normal description.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n The skill description when it's locked.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n Icon to be displayed for the Skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n A list of icons to show in the tooltip of this\n skill.\n ', tunable=TunableIcon(description='\n Icon that is displayed what types of objects help\n improve this skill.\n '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n Tutorial instance for this skill. This will be used to bring up the\n skill lesson from the first notification for Sim to know this skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n Skill priority. Higher priority skills will trump other skills when\n being displayed on the UI. When a Sim gains multiple skills at the\n same time, only the highest priority one will display a progress bar\n over the Sim's head.\n ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n Tooltip which describes what the next level entails.\n ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n When this mood is set and active sim matches mood, the UI will\n display a special effect on the skill bar to represent that this\n skill is getting a bonus because of the mood.\n ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n If checked, this skill will be hidden.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n Whether this skill will send update messages to the client\n for non-active household sims (NPCs).\n \n e.g. A toddler's communication skill determines the VOX they use, so\n the client needs to know the skill level for all toddlers in order\n for this work properly.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n Whether Sim will default has this skill.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n Allowed ages for this skill.\n ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2(description='\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n The weight of the Skill with regards to autonomy. It's ignored for\n the purposes of sorting stats, but it's applied when scoring the\n actual statistic operation for the SI.\n ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n Multipliers this skill applies to other statistics based on its\n value.\n ', key_type=TunableReference(description='\n The statistic this multiplier will be applied to.\n ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n Tunable curve where the X-axis defines the skill level, and\n the Y-axis defines the associated multiplier.\n ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n Direction where the multiplier should work on the\n statistic. For example, a tuned decrease for an object's\n brokenness rate will not also increase the time it takes to\n repair it.\n ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n If checked, this modifier will look at the current\n effective skill value. If unchecked, this modifier will\n look at the actual skill value.\n ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n Multipliers this skill applies to the success chance of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n Multipliers this skill applies to the monetary payout amount of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n The associated categories of the skill\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n Skill level list to use.\n ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n Level-specific information, such as notifications to be displayed to\n level up.\n ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n The notification to display when the Sim obtains this level.\n The text will be provided two tokens: the Sim owning the\n skill and a number representing the 1-based skill level\n ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n Screen slam to show when reaches this skill level.\n Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n {1.String}, Skill Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n The buff to place on a Sim when they reach this specific\n level of skill.\n ', manager=services.buff_manager())), rewards=TunableList(description='\n A reward to give for achieving this level.\n ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n A loot to apply for achieving this level.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n Super affordances this adds to the Sim.\n ', tunable=TunableReference(description='\n A super affordance added to this Sim.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n Super affordances this adds to the target.\n ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n Mixers this adds to an associated actor object. (When targeting\n something else.)\n ', key_type=TunableReference(description='\n The super affordance these mixers are associated with.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n Set of mixer affordances associated with the super affordance.\n ', tunable=TunableReference(description='\n Linked mixer affordance.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n Data used to modify the value of a new skill based on the level\n of this skill.\n \n e.g. Toddler Communication skill transfers into Child Social skill.\n ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n The new skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n A mapping between this skill's levels and the\n new skill's internal value.\n \n The keys are user facing skill levels.\n \n The values are the internal statistic value, not the user\n facing skill level.\n ", key_type=Tunable(description="\n This skill's level.\n \n This is the actual user facing skill level.\n ", tunable_type=int, default=0), value_type=Tunable(description='\n The new skill\'s value.\n \n This is the internal statistic\n value, not the user facing skill level."\n ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n A list of skills that become unlocked when this skill is maxed.\n ', tunable=TunableReference(description='\n A skill to unlock.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n If enabled, we associate this skill with a particular trend via tag\n which you can find in trend_tuning.\n ', tunable=TunableTag(description='\n The trend tag we associate with this skill\n ', filter_prefixes=('func_trend',)))} REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value') def __init__(self, tracker): self._skill_level_buff = None super().__init__(tracker, self.initial_value) self._delta_enabled = True self._max_level_update_sent = False @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() level_list = cls.get_level_list() cls.max_level = len(level_list) cls.min_value_tuning = 0 cls.max_value_tuning = sum(level_list) cls._default_convergence_value = cls.min_value_tuning cls._build_utility_curve_from_tuning_data(cls.ad_data) for stat in cls.statistic_multipliers: multiplier = cls.statistic_multipliers[stat] curve = multiplier.curve direction = multiplier.direction use_effective_skill = multiplier.use_effective_skill stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill) for multiplier in cls.success_chance_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill) for multiplier in cls.monetary_payout_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill) @classmethod def _verify_tuning_callback(cls): success_multiplier_affordances = [] for multiplier in cls.success_chance_multipliers: success_multiplier_affordances.extend(multiplier.affordance_list) if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') monetary_payout_multiplier_affordances = [] for multiplier in cls.monetary_payout_multipliers: monetary_payout_multiplier_affordances.extend(multiplier.affordance_list) if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') @classproperty def skill_type(cls): return cls @constproperty def is_skill(): return True @classproperty def autonomy_weight(cls): return cls.weight @constproperty def remove_on_convergence(): return False @classproperty def valid_for_stat_testing(cls): return True @classmethod def can_add(cls, owner, force_add=False, **kwargs): if force_add: return True if owner.age not in cls.ages: return False return super().can_add(owner, **kwargs) @classmethod def convert_to_user_value(cls, value): level_list = cls.get_level_list() if not level_list: return 0 current_value = value for (level, level_threshold) in enumerate(level_list): current_value -= level_threshold if current_value < 0: return level return level + 1 @classmethod def convert_from_user_value(cls, user_value): (level_min, _) = cls._get_level_bounds(user_value) return level_min @classmethod def create_skill_update_msg(cls, sim_id, stat_value): skill_msg = Commodities_pb2.Skill_Update() skill_msg.skill_id = cls.guid64 skill_msg.curr_points = int(stat_value) skill_msg.sim_id = sim_id return skill_msg @classmethod def get_level_list(cls): return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type) @classmethod def get_skill_effectiveness_points_gain(cls, effectiveness_level, level): skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level) if skill_gain_curve is not None: return skill_gain_curve.get(level) logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level) return 0 def _get_level_data_for_skill_level(self, skill_level): level_data = self.level_data.get(skill_level) if level_data is None: logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level) return level_data @property def is_initial_value(self): return self.initial_value == self.get_value() def should_send_update(self, sim_info, stat_value): if sim_info.is_npc and not self.update_client_for_npcs: return False if self.hidden: return False if Skill.convert_to_user_value(stat_value) == 0: return False if self.reached_max_level: if self._max_level_update_sent: return False self._max_level_update_sent = True return True def on_initial_startup(self): super().on_initial_startup() skill_level = self.get_user_value() self._update_skill_level_buff(skill_level) def on_add(self): super().on_add() self._tracker.owner.add_modifiers_for_skill(self) level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is not None: provided_affordances = [] for provided_affordance in level_data.target_super_affordances: provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self) provided_affordances.append(provided_affordance_data) self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances) self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers) sim = self._tracker._owner.get_sim_instance() apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances) def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self._destory_callback_handle() if not on_destroy: self._send_skill_delete_message() if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if not on_destroy: self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) def on_zone_load(self): self._max_level_update_sent = False def _apply_multipliers_to_continuous_statistics(self): for stat in self.statistic_multipliers: if stat.continuous: owner_stat = self.tracker.get_statistic(stat) if owner_stat is not None: owner_stat._recalculate_modified_decay_rate() @classproperty def default_value(cls): return cls.initial_value @flexmethod @caches.cached def get_user_value(cls, inst): inst_or_cls = inst if inst is not None else cls return super(__class__, inst_or_cls).get_user_value() def _clear_user_value_cache(self): self.get_user_value.func.cache.clear() def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self.get_value() super().set_value(value, *args, **kwargs) if not caches.skip_cache: self._clear_user_value_cache() if from_load: return event_manager = services.get_event_manager() sim_info = self._tracker._owner new_value = self.get_value() new_level = self.convert_to_user_value(value) if old_value == self.initial_value or old_value != new_value: event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) if old_level < new_level or old_value == self.initial_value: self._apply_multipliers_to_continuous_statistics() event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def add_value(self, add_amount, interaction=None, **kwargs): old_value = self.get_value() if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME else: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION super().add_value(add_amount, interaction=interaction) if not caches.skip_cache: self._clear_user_value_cache() if interaction is not None: interaction_name = interaction.affordance.__name__ else: interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name) def _update_value(self): old_value = self._value if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled: last_update = self._last_update time_delta = super()._update_value() if not caches.skip_cache: self._clear_user_value_cache() new_value = self._value if old_value < new_value: event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE) event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) new_level = self.convert_to_user_value(new_value) if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim: gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update) if old_level < new_level or old_value == self.initial_value: if self._tracker is not None: self._tracker.notify_watchers(self.stat_type, self._value, self._value) event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) if not self.reached_max_level: return event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) def on_skill_updated(self, telemhook, old_value, new_value, affordance_name): owner_sim_info = self._tracker._owner if owner_sim_info.is_selectable: with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name) hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True) hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value) if old_value == self.initial_value: skill_level = self.convert_to_user_value(old_value) self._handle_skill_up(skill_level) def _send_skill_delete_message(self): if self.tracker.owner.is_npc: return skill_msg = Commodities_pb2.SkillDelete() skill_msg.skill_id = self.guid64 op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg) Distributor.instance().add_op(self.tracker.owner, op) @staticmethod def _callback_handler(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_threshold_callback() def _handle_skill_up(self, skill_level): self._show_level_notification(skill_level) self._update_skill_level_buff(skill_level) self._try_give_skill_up_payout(skill_level) self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) super_affordances = tuple(self._tracker.get_cached_super_affordances_gen()) apply_super_affordance_commodity_flags(sim, self, super_affordances) def _recalculate_modified_decay_rate(self): pass def refresh_level_up_callback(self): self._destory_callback_handle() def _on_level_up_callback(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_level_up_callback() self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback) def on_skill_level_up(self, old_level, new_level): tracker = self.tracker sim_info = tracker._owner if self.reached_max_level: for skill in self.skill_unlocks_on_max: skill_instance = tracker.add_statistic(skill, force_add=True) skill_instance.set_value(skill.initial_value) with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level) self._handle_skill_up(new_level) services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,)) def _show_level_notification(self, skill_level, ignore_npc_check=False): sim_info = self._tracker._owner if not (ignore_npc_check or not sim_info.is_npc): if skill_level == 1: tutorial_service = services.get_tutorial_service() if tutorial_service is not None and tutorial_service.is_tutorial_running(): return level_data = self._get_level_data_for_skill_level(skill_level) if level_data is not None: tutorial_id = None if self.tutorial is not None: if skill_level == 1: tutorial_id = self.tutorial.guid64 notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id) if level_data.level_up_screen_slam is not None: level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level) def _update_skill_level_buff(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) new_buff = level_data.skill_level_buff if level_data is not None else None if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if new_buff is not None: self._skill_level_buff = self._tracker.owner.add_buff(new_buff) def _try_give_skill_up_payout(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) if level_data is None: return if level_data.rewards: for reward in level_data.rewards: reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self) if level_data.loot: resolver = SingleSimResolver(self._tracker.owner) for loot in level_data.loot: loot.apply_to_resolver(resolver) def force_show_level_notification(self, skill_level): self._show_level_notification(skill_level, ignore_npc_check=True) @classmethod def send_commodity_update_message(cls, sim_info, old_value, new_value): stat_instance = sim_info.get_statistic(cls.stat_type, add=False) if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value): return msg = cls.create_skill_update_msg(sim_info.id, new_value) add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False) change_rate = stat_instance.get_change_rate() hide_progress_bar = False if sim_info.is_npc or sim_info.is_skill_bar_suppressed(): hide_progress_bar = True op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar) distributor.ops.record(sim_info, op) def save_statistic(self, commodities, skills, ranked_stats, tracker): current_value = self.get_saved_value() if current_value == self.initial_value: return message = protocols.Skill() message.name_hash = self.guid64 message.value = current_value if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks() skills.append(message) def unlocks_skills_on_max(self): return True def can_decay(self): return False def get_skill_provided_affordances(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return ((), ()) return (level_data.super_affordances, level_data.target_super_affordances) def get_skill_provided_actor_mixers(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return return level_data.actor_mixers def get_actor_mixers(self, super_interaction): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return [] mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else [] return mixers @flexmethod def populate_localization_token(cls, inst, token): inst_or_cls = inst if inst is not None else cls token.type = LocalizedStringToken.STRING token.text_string = inst_or_cls.stat_name
class AuditionDramaNode(BaseDramaNode): INSTANCE_TUNABLES = { 'gig': Gig.TunableReference( description='\n Gig this audition is for.\n ' ), 'audition_prep_time': TunableTimeSpan( description= '\n Amount of time between the seed of the potential audition node\n to the start of the audition time. \n ', default_hours=5), 'audition_prep_recommendation': TunableLocalizedStringFactory( description= '\n String that gives the player more information on how to succeed\n in this audition.\n ' ), 'audition_prep_icon': OptionalTunable( description= '\n If enabled, this icon will be displayed with the audition preparation.\n ', tunable=TunableIcon( description= '\n Icon for audition preparation.\n ' )), 'audition_outcomes': TunableList( description= '\n List of loot and multipliers which are for audition outcomes.\n ', tunable=TunableTuple( description= '\n The information needed to determine whether or not the sim passes\n or fails this audition. We cannot rely on the outcome of the \n interaction because we need to run this test on uninstantiated \n sims as well. This is similar to the fallback outcomes in \n interactions.\n ', loot_list=TunableList( description= '\n Loot applied if this outcome is chosen\n ', tunable=LootActions.TunableReference(pack_safe=True)), weight=TunableMultiplier.TunableFactory( description= '\n A tunable list of tests and multipliers to apply to the \n weight of the outcome.\n ' ), is_success=Tunable( description= '\n Whether or not this is considered a success outcome.\n ', tunable_type=bool, default=False))), 'audition_rabbit_hole': RabbitHole.TunableReference( description= '\n Data required to put sim in rabbit hole.\n ' ), 'skip_audition': OptionalTunable( description= '\n If enabled, we can skip auditions if sim passes tuned tests.\n ', tunable=TunableTuple( description= '\n Data related to whether or not this audition can be skipped.\n ', skip_audition_tests=TunableTestSet( description= '\n Test to see if sim can skip this audition.\n ' ), skipped_audition_loot=TunableList( description= '\n Loot applied if sim manages to skip audition\n ', tunable=LootActions.TunableReference(pack_safe=True)))), 'advance_notice_time': TunableTimeSpan( description= '\n The amount of time between the alert and the start of the event.\n ', default_hours=1, locked_args={ 'days': 0, 'minutes': 0 }), 'loot_on_schedule': TunableList( description= '\n Loot applied if the audition drama node is scheduled successfully.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'advance_notice_notification': TunableUiDialogNotificationSnippet( description= '\n The notification that is displayed at the advance notice time.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._calculated_audition_time = None self._calculated_gig_time = None self._rabbit_hole_id = None @classproperty def drama_node_type(cls): return DramaNodeType.AUDITION @property def _require_instanced_sim(self): return False @classproperty def persist_when_active(cls): return True def get_picker_schedule_time(self): return self._calculated_audition_time def create_picker_row(self, owner=None, **kwargs): now_time = services.game_clock_service().now() min_audition_time = now_time + self.audition_prep_time() possible_audition_times = self.get_final_times_based_on_schedule( self.min_and_max_times, anchor_time=min_audition_time, scheduled_time_only=True) audition_time = min_audition_time if possible_audition_times is not None: now = services.time_service().sim_now for possible_audition_time in possible_audition_times: if possible_audition_time[0] >= now: audition_time = possible_audition_time[0] break gig = self.gig time_till_gig = gig.get_time_until_next_possible_gig(audition_time) if time_till_gig is None: return gig_time = audition_time + time_till_gig if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests( SingleSimResolver(owner)): formatted_string = Career.GIG_PICKER_SKIPPED_AUDITION_LOCALIZATION_FORMAT( gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, gig_time, self.audition_prep_recommendation()) else: formatted_string = Career.GIG_PICKER_LOCALIZATION_FORMAT( gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, audition_time, gig_time, self.audition_prep_recommendation()) self._calculated_audition_time = audition_time self._calculated_gig_time = gig_time return gig.create_picker_row(formatted_string, owner) def schedule(self, resolver, specific_time=None, time_modifier=TimeSpan.ZERO): if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests( resolver): for loot in self.skip_audition.skipped_audition_loot: loot.apply_to_resolver(resolver) resolver.sim_info_to_test.career_tracker.set_gig( self.gig, self._calculated_gig_time) return False success = super().schedule(resolver, specific_time=specific_time, time_modifier=time_modifier) if success: services.calendar_service().mark_on_calendar( self, advance_notice_time=self.advance_notice_time()) self._send_career_ui_update(is_add=True) for loot in self.loot_on_schedule: loot.apply_to_resolver(resolver) return success def cleanup(self, from_service_stop=False): services.calendar_service().remove_on_calendar(self.uid) self._send_career_ui_update(is_add=False) rabbit_hole_service = services.get_rabbit_hole_service() if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole( self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): rabbit_hole_service.remove_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) super().cleanup(from_service_stop=from_service_stop) def resume(self): if self._rabbit_hole_id and not services.get_rabbit_hole_service( ).is_in_rabbit_hole(self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): services.drama_scheduler_service().complete_node(self.uid) def _run(self): rabbit_hole_service = services.get_rabbit_hole_service() self._rabbit_hole_id = rabbit_hole_service.put_sim_in_managed_rabbithole( self._receiver_sim_info, self.audition_rabbit_hole) if self._rabbit_hole_id is None: self._on_sim_return(canceled=True) rabbit_hole_service.set_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def _on_sim_return(self, canceled=False): receiver_sim_info = self._receiver_sim_info resolver = SingleSimResolver(receiver_sim_info) weights = [] failure_outcomes = [] for outcome in self.audition_outcomes: if canceled: if not outcome.is_success: failure_outcomes.append(outcome) weight = outcome.weight.get_multiplier(resolver) if weight > 0: weights.append((weight, outcome)) else: weight = outcome.weight.get_multiplier(resolver) if weight > 0: weights.append((weight, outcome)) if failure_outcomes: selected_outcome = random.choice(failure_outcomes) else: selected_outcome = sims4.random.weighted_random_item(weights) if not selected_outcome: logger.error( 'No valid outcome is tuned on this audition. Verify weights in audition_outcome for {}.', self.guid64) services.drama_scheduler_service().complete_node(self.uid) return if selected_outcome.is_success: receiver_sim_info.career_tracker.set_gig(self.gig, self._calculated_gig_time) for loot in selected_outcome.loot_list: loot.apply_to_resolver(resolver) services.drama_scheduler_service().complete_node(self.uid) def _save_custom_data(self, writer): if self._calculated_audition_time is not None: writer.write_uint64(AUDITION_TIME_TOKEN, self._calculated_audition_time) if self._calculated_gig_time is not None: writer.write_uint64(GIG_TIME_TOKEN, self._calculated_gig_time) if self._rabbit_hole_id is not None: writer.write_uint64(RABBIT_HOLE_ID_TOKEN, self._rabbit_hole_id) def _load_custom_data(self, reader): self._calculated_audition_time = DateAndTime( reader.read_uint64(AUDITION_TIME_TOKEN, None)) self._calculated_gig_time = DateAndTime( reader.read_uint64(GIG_TIME_TOKEN, None)) self._rabbit_hole_id = reader.read_uint64(RABBIT_HOLE_ID_TOKEN, None) rabbit_hole_service = services.get_rabbit_hole_service() if not self._rabbit_hole_id: rabbit_hole_service = services.get_rabbit_hole_service() self._rabbit_hole_id = services.get_rabbit_hole_service( ).get_rabbit_hole_id_by_type(self._receiver_sim_info.id, self.audition_rabbit_hole) if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole( self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): rabbit_hole_service.set_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) self._send_career_ui_update() return True def _send_career_ui_update(self, is_add=True): audition_update_msg = DistributorOps_pb2.AuditionUpdate() if is_add: self.gig.build_gig_msg( audition_update_msg.audition_info, self._receiver_sim_info, gig_time=self._calculated_gig_time, audition_time=self._calculated_audition_time) op = GenericProtocolBufferOp(Operation.AUDITION_UPDATE, audition_update_msg) build_icon_info_msg( IconInfoData(icon_resource=self.audition_prep_icon), self.audition_prep_recommendation(), audition_update_msg.recommended_task) Distributor.instance().add_op(self._receiver_sim_info, op) def load(self, drama_node_proto, schedule_alarm=True): super_success = super().load(drama_node_proto, schedule_alarm=schedule_alarm) if not super_success: return False services.calendar_service().mark_on_calendar( self, advance_notice_time=self.advance_notice_time()) return True def on_calendar_alert_alarm(self): receiver_sim_info = self._receiver_sim_info resolver = SingleSimResolver(receiver_sim_info) dialog = self.advance_notice_notification(receiver_sim_info, resolver=resolver) dialog.show_dialog()
class FestivalDramaNode(BaseDramaNode): GO_TO_FESTIVAL_INTERACTION = TunablePackSafeReference(description='\n Reference to the interaction used to travel the Sims to the festival.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)) INSTANCE_TUNABLES = {'festival_open_street_director': TunableReference(description='\n Reference to the open street director in question.\n ', manager=services.get_instance_manager(sims4.resources.Types.OPEN_STREET_DIRECTOR)), 'street': TunableReference(description='\n The street that this festival is allowed to run on.\n ', manager=services.get_instance_manager(sims4.resources.Types.STREET)), 'scoring': OptionalTunable(description='\n If enabled this DramaNode will be scored and chosen by the drama\n service.\n ', tunable=TunableTuple(description='\n Data related to scoring this DramaNode.\n ', base_score=TunableRange(description='\n The base score of this drama node. This score will be\n multiplied by the score of the different filter results\n used to find the Sims for this DramaNode to find the final\n result.\n ', tunable_type=int, default=1, minimum=1), bucket=TunableEnumEntry(description="\n Which scoring bucket should these drama nodes be scored as\n part of. Only Nodes in the same bucket are scored against\n each other.\n \n Change different bucket settings within the Drama Node's\n module tuning.\n ", tunable_type=DramaNodeScoringBucket, default=DramaNodeScoringBucket.DEFAULT), locked_args={'receiving_sim_scoring_filter': None})), 'pre_festival_duration': TunableSimMinute(description='\n The amount of time in Sim minutes that this festival will be in a\n pre-running state. Testing against this Drama Node will consider\n the node to be running, but the festival will not actually be.\n ', default=120, minimum=1), 'fake_duration': TunableSimMinute(description="\n The amount of time in Sim minutes that we will have this drama node\n run when the festival isn't actually up and running. When the\n festival actually runs we will trust in the open street director to\n tell us when we should actually end.\n ", default=60, minimum=1), 'festival_dynamic_sign_info': OptionalTunable(description='\n If enabled then this festival drama node can be used to populate\n a dynamic sign.\n ', tunable=TunableTuple(description='\n Data for populating the dynamic sign view for the festival.\n ', festival_name=TunableLocalizedString(description='\n The name of this festival.\n '), festival_time=TunableLocalizedString(description='\n The time that this festival should run.\n '), travel_to_festival_text=TunableLocalizedString(description='\n The text that will display to get you to travel to the festival.\n '), festival_not_started_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the festival has not started.\n '), on_street_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the player is already at the festival.\n '), on_vacation_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the player is on vacation.\n '), display_image=TunableResourceKey(description='\n The image for this festival display.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), background_image=TunableResourceKey(description='\n The background image for this festival display.\n ', default=None, resource_types=sims4.resources.CompoundTypes.IMAGE), activity_info=TunableList(description='\n The different activities that are advertised to be running at this\n festival.\n ', tunable=TunableTuple(description='\n A single activity that will be taking place at this festival.\n ', activity_name=TunableLocalizedString(description='\n The name of this activity.\n '), activity_description=TunableLocalizedString(description='\n The description of this activity.\n '), icon=TunableIcon(description='\n The Icon that represents this festival activity.\n ')))), tuning_group=GroupNames.UI), 'starting_notification': OptionalTunable(description='\n If enabled then when this festival runs we will surface a\n notification to the players.\n ', tunable=TunableTestedUiDialogNotificationSnippet(description='\n The notification that will appear when this drama node runs.\n '), tuning_group=GroupNames.UI), 'additional_drama_nodes': TunableList(description='\n A list of additional drama nodes that we will score and schedule\n when this drama node is run. Only 1 drama node is run.\n ', tunable=TunableReference(description='\n A drama node that we will score and schedule when this drama\n node is run.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE))), 'delay_timeout': TunableSimMinute(description='\n The amount of time in Sim minutes that the open street director has\n been delayed that we will no longer start the festival.\n ', default=120, minimum=0), 'travel_lot_override': OptionalTunable(description='\n If enabled, sims will spawn at this lot instead of the Travel Lot \n tuned on the street.\n ', tunable=TunableLotDescription(description='\n The specific lot that we will travel to when asked to travel to\n this street.\n ')), 'reject_same_street_travel': Tunable(description='\n If True, we will disallow the drama node travel interaction to run\n if the Sim is on the same street as the destination zone. If False,\n same street travel will be allowed.\n ', tunable_type=bool, default=True)} REMOVE_INSTANCE_TUNABLES = ('receiver_sim', 'sender_sim_info', 'picked_sim_info') @classproperty def drama_node_type(cls): return DramaNodeType.FESTIVAL @classproperty def persist_when_active(cls): return True @classproperty def simless(cls): return True @classmethod def get_travel_lot_id(cls, reject_same_street=False): if reject_same_street and cls.street is services.current_street(): return if cls.travel_lot_override is not None: return get_lot_id_from_instance_id(cls.travel_lot_override) return get_lot_id_from_instance_id(cls.street.travel_lot) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._duration_alarm = None self._additional_nodes_processor = None def cleanup(self, from_service_stop=False): super().cleanup(from_service_stop=from_service_stop) if self._duration_alarm is not None: alarms.cancel_alarm(self._duration_alarm) self._duration_alarm = None if self._additional_nodes_processor is not None: self._additional_nodes_processor.trigger_hard_stop() self._additional_nodes_processor = None def _alarm_finished_callback(self, _): services.drama_scheduler_service().complete_node(self.uid) def _request_timed_out_callback(self): services.drama_scheduler_service().complete_node(self.uid) def _open_street_director_destroyed_early_callback(self): services.drama_scheduler_service().complete_node(self.uid) def _get_time_till_end(self): now = services.time_service().sim_now time_since_started = now - self._selected_time duration = create_time_span(minutes=self.fake_duration + self.pre_festival_duration) time_left_to_go = duration - time_since_started return time_left_to_go def _setup_end_alarm(self): time_left_to_go = self._get_time_till_end() self._duration_alarm = alarms.add_alarm(self, time_left_to_go, self._alarm_finished_callback) def _create_open_street_director_request(self): festival_open_street_director = self.festival_open_street_director(drama_node_uid=self._uid) preroll_time = self._selected_time + create_time_span(minutes=self.pre_festival_duration) request = OpenStreetDirectorRequest(festival_open_street_director, priority=festival_open_street_director.priority, preroll_start_time=preroll_time, timeout=create_time_span(minutes=self.delay_timeout), timeout_callback=self._request_timed_out_callback, premature_destruction_callback=self._open_street_director_destroyed_early_callback) services.venue_service().request_open_street_director(request) def _try_and_start_festival(self): street = services.current_street() if street is not self.street: self._setup_end_alarm() return self._create_open_street_director_request() def _process_scoring_gen(self, timeline): try: yield from services.drama_scheduler_service().score_and_schedule_nodes_gen(self.additional_drama_nodes, 1, street_override=self.street, timeline=timeline) except GeneratorExit: raise except Exception as exception: logger.exception('Exception while scoring DramaNodes: ', exc=exception, level=sims4.log.LEVEL_ERROR) finally: self._additional_nodes_processor = None def _pre_festival_alarm_callback(self, _): self._try_and_start_festival() services.get_event_manager().process_events_for_household(TestEvent.FestivalStarted, services.active_household()) if self.starting_notification is not None: resolver = GlobalResolver() starting_notification = self.starting_notification(services.active_sim_info(), resolver=resolver) starting_notification.show_dialog(response_command_tuple=tuple([CommandArgType.ARG_TYPE_INT, self.guid64])) if self.additional_drama_nodes: sim_timeline = services.time_service().sim_timeline self._additional_nodes_processor = sim_timeline.schedule(elements.GeneratorElement(self._process_scoring_gen)) def _setup_pre_festival_alarm(self): now = services.time_service().sim_now time_since_started = now - self._selected_time duration = create_time_span(minutes=self.pre_festival_duration) time_left_to_go = duration - time_since_started self._duration_alarm = alarms.add_alarm(self, time_left_to_go, self._pre_festival_alarm_callback) def _run(self): self._setup_pre_festival_alarm() services.get_event_manager().process_events_for_household(TestEvent.FestivalStarted, services.active_household()) return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def resume(self): now = services.time_service().sim_now time_since_started = now - self._selected_time if time_since_started < create_time_span(minutes=self.pre_festival_duration): self._setup_pre_festival_alarm() else: self._try_and_start_festival() def is_on_festival_street(self): street = services.current_street() return street is self.street def is_during_pre_festival(self): now = services.time_service().sim_now time_since_started = now - self._selected_time if time_since_started < create_time_span(minutes=self.pre_festival_duration): return True return False @classmethod def show_festival_info(cls): if cls.festival_dynamic_sign_info is None: return ui_info = cls.festival_dynamic_sign_info festival_info = UI_pb2.DynamicSignView() festival_info.drama_node_guid = cls.guid64 festival_info.name = ui_info.festival_name lot_id = cls.get_travel_lot_id() persistence_service = services.get_persistence_service() zone_id = persistence_service.resolve_lot_id_into_zone_id(lot_id, ignore_neighborhood_id=True) zone_protobuff = persistence_service.get_zone_proto_buff(zone_id) if zone_protobuff is not None: festival_info.venue = LocalizationHelperTuning.get_raw_text(zone_protobuff.name) festival_info.time = ui_info.festival_time festival_info.image = sims4.resources.get_protobuff_for_key(ui_info.display_image) festival_info.background_image = sims4.resources.get_protobuff_for_key(ui_info.background_image) festival_info.action_label = ui_info.travel_to_festival_text running_nodes = services.drama_scheduler_service().get_running_nodes_by_class(cls) active_sim_info = services.active_sim_info() if all(active_node.is_during_pre_festival() for active_node in running_nodes): festival_info.disabled_tooltip = ui_info.festival_not_started_tooltip elif any(active_node.is_on_festival_street() for active_node in running_nodes): festival_info.disabled_tooltip = ui_info.on_street_tooltip elif active_sim_info.is_in_travel_group(): festival_info.disabled_tooltip = ui_info.on_vacation_tooltip for activity in ui_info.activity_info: with ProtocolBufferRollback(festival_info.activities) as activity_msg: activity_msg.name = activity.activity_name activity_msg.description = activity.activity_description activity_msg.icon = create_icon_info_msg(IconInfoData(activity.icon)) distributor = Distributor.instance() distributor.add_op(active_sim_info, GenericProtocolBufferOp(Operation.DYNAMIC_SIGN_VIEW, festival_info)) @classmethod def travel_to_festival(cls): active_sim_info = services.active_sim_info() active_sim = active_sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED) if active_sim is None: return lot_id = cls.get_travel_lot_id(reject_same_street=cls.reject_same_street_travel) if lot_id is None: return pick = PickInfo(pick_type=PickType.PICK_TERRAIN, lot_id=lot_id, ignore_neighborhood_id=True) context = interactions.context.InteractionContext(active_sim, interactions.context.InteractionContext.SOURCE_SCRIPT_WITH_USER_INTENT, interactions.priority.Priority.High, insert_strategy=interactions.context.QueueInsertStrategy.NEXT, pick=pick) active_sim.push_super_affordance(FestivalDramaNode.GO_TO_FESTIVAL_INTERACTION, None, context)
class NotebookEntry(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.NOTEBOOK_ENTRY)): INSTANCE_TUNABLES = { 'category_id': TunableEnumEntry( description= '\n Category type which will define the format the UI will use\n to display the information.\n ', tunable_type=NotebookCategories, default=NotebookCategories.INVALID), 'subcategory_id': TunableEnumEntry( description= '\n Subcategory type which will define the format the UI will use\n to display the information.\n ', tunable_type=NotebookSubCategories, default=NotebookSubCategories.INVALID), 'entry_text': TunableLocalizedString( description= '\n Text to be displayed on the notebook entry. \n ' ), 'entry_icon': OptionalTunable( TunableIcon( description= '\n Optional icon to be displayed with the entry text.\n ' )), 'entry_tooltip': OptionalTunable( TunableTuple( description= '\n Text to be displayed when the player hovers this entry.\n ', tooltip_style=TunableEnumEntry( description= '\n Types of possible tooltips that can be displayed for an entry. \n ', tunable_type=HovertipStyle, default=HovertipStyle.HOVER_TIP_DEFAULT), tooltip_fields=TunableMapping( description= '\n Mapping of tooltip fields to its localized values. Since \n this fields are created from a system originally created \n for recipes, all of them may be tuned, but these are the \n most common fields to show on a tooltip:\n - recipe_name = This is the actual title of the tooltip. \n This is the main text\n - recipe_description = This description refers to the main \n text that will show below the title\n - header = Smaller text that will show just above the title\n - subtext = Smaller text that will show just bellow the \n title\n ', key_type=TunableEnumEntry( description= '\n Fields to be populated in the tooltip. These fields\n will be populated with the text and tokens tuned.\n ', tunable_type=TooltipFields, default=TooltipFields.recipe_name), value_type=TunableLocalizedString()))), 'entry_sublist': OptionalTunable( TunableList( description= '\n List of objects linked to a notebook entry.\n i.e. Ingredient objects attached to a serum or to a recipe.\n ', tunable=TunableTuple( description= '\n Pair of object definitions and amount of objects needed\n to \n ', object_definition=TunableReference( services.definition_manager(), description='Reference to ingredient object.'), num_objects_required=Tunable( description= '\n Number of objects required on this field. This will be\n displayed next to the current value of objects found in the \n inventory.\n Example: Serums will displayed \n <current_objects_held / num_objects_required>\n ', tunable_type=int, default=0)))), 'entry_sublist_is_sortable': OptionalTunable( description= '\n If enabled, entry sublist will be presented sorted alphabetically.\n ', tunable=TunableTuple(include_new_entry=Tunable( description= '\n If checked, the new entry in entry sublist will be sorted.\n ', tunable_type=bool, default=False))) } def __init__(self, entry_object_definition_id=None, sub_entries=None, new_entry=True): self.new_entry = new_entry self.entry_object_definition_id = entry_object_definition_id if sub_entries is not None: self.sub_entries = list(sub_entries) else: self.sub_entries = list() def has_identical_entries(self, entries): for entry in entries: if self.__class__ == entry.__class__: return True return False def is_definition_based(self): return False @property def entry_icon_info_data(self): if self.entry_icon is not None: return IconInfoData(icon_resource=self.entry_icon)
class UiTuning: LOADING_SCREEN_STRINGS = TunableMapping( description= '\n Mapping from the Pack to its associated loading strings.\n ', key_type=TunableEnumEntry( description= '\n The pack containing the strings.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_type=TunableList( description= '\n The list of loading screen strings which belongs to the pack.\n We always display the strings from base game AND from the latest\n pack which the player is entitled to and has installed. \n ', tunable=TunableLocalizedString()), export_modes=(ExportModes.ClientBinary, ), tuple_name='LoadingScreenStringsTuple') GO_HOME_INTERACTION = TunableReference( description= '\n The interaction to push a Sim to go home.\n ', manager=services.affordance_manager(), export_modes=(ExportModes.ClientBinary, )) COME_NEAR_ACTIVE_SIM = TunableReference( description= '\n An affordance to push on a Sim so they come near the active Sim.\n ', manager=services.affordance_manager()) BRING_HERE_INTERACTION = TunableReference( description= '\n An affordance to push on household members to summon them to the\n current lot if they are not instanced.\n ', manager=services.affordance_manager()) NEW_CONTENT_ALERT_TUNING = TunableMapping( description= '\n Mapping from Pack to its associated new content alert tuning\n ', key_type=TunableEnumEntry( description= '\n The pack containing the new content tuning. NOTE: this should never\n be tuned to BASE_GAME. That would trigger for all users.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_type=TunableTuple( description= '\n Each pack will have a set of tuning of images and text to display\n to inform the user what new features have been introduced in the \n pack.\n ', export_class_name='TunablePackContentTuple', title=TunableLocalizedString( description= '\n The title to be displayed at the top of the New Content Alert\n UI for this pack.\n ' ), cycle_images=TunableList( description= '\n A list of images (screenshots) that the UI cycles through to\n show off some of the new features.\n ', tunable=TunableResourceKey( resource_types=sims4.resources.CompoundTypes.IMAGE)), feature_list=TunableList( description= '\n A list of tuples that describe each new feature in the New\n Content Alert UI. NOTE: This should NEVER have more than 4\n elements in it.\n ', maxlength=4, tunable=TunableTuple( description= '\n A tuple that contains title text, description, an icon,\n and a reference to the matching lesson for this new \n feature.\n ', export_class_name='TunableFeatureTuple', title_text=TunableLocalizedString( description= '\n A title to be displayed in bold for the feature.\n ' ), description_text=TunableLocalizedString( description= '\n A short description of the new feature.\n ' ), icon=TunableResourceKey( description= '\n An icon that represents the feature.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), lesson=TunableReference( description= '\n A reference to the lesson that the user can go look at\n for this new feature.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL), allow_none=True, pack_safe=True)))), export_modes=(ExportModes.ClientBinary, ), tuple_name='NewContentAlertTuple') PACK_SPECIFIC_DATA = TunableMapping( description= '\n Mapping from a Pack to its associated data. This includes pack icons,\n filter strings, and the credits file.\n ', key_name='packId', key_type=TunableEnumEntry( description= '\n The pack id for the associated data.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_name='packData', value_type=TunableTuple( description= '\n Each pack will have a set icons and can have an optional filter \n string for use in Build/CAS and an optional Credits Title\n ', export_class_name='TunablePackDataTuple', credits_title=TunableLocalizedString( description= '\n The title used in the credits dropdown to select this packs credits.\n If set, there must be a creditsxml file for this pack\n in Assets/InGame/UI/Flash/data/\n ', allow_none=True), filter_name=TunableLocalizedString( description= '\n The name to used to describe the pack in CAS and BuildBuy filters.\n If set, this pack will appear in the filter list.\n ', allow_none=True), pack_type=TunableEnumEntry( description= '\n Which type of pack is this.\n ', tunable_type=PackTypes, default=PackTypes.BASE), icon_32=TunableResourceKey( description= '\n Pack icon. 32x32.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_64=TunableResourceKey( description= '\n Pack icon. 64x64.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_128=TunableResourceKey( description= '\n Pack icon. 128x128.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_owned=TunableIcon( description= '\n Pack icon that is displayed in the main menu\n pack display when the player owns that pack.\n ', allow_none=True), icon_unowned=TunableIcon( description= '\n Pack icon that is displayed in the main menu\n pack display when the player does not own that pack.\n ', allow_none=True), webstore_id=Tunable( description= '\n web store pack specific url identifier\n\t\t\t\t', tunable_type=str, default=None), region_list=TunableList( description= '\n A list of tuples that describe each new region in the pack.\n ', tunable=TunableTuple( description= '\n A tuple that contains metadata for a world select region.\n ', export_class_name='TunablePackRegionTuple', region_resource=TunableRegionDescription( description= '\n Reference to the region description catalog resource associated with this region\n ', pack_safe=True), is_player_facing=Tunable( description= '\n Whether to display this region in world select when the user does not own the associated pack\n ', tunable_type=bool, default=False), region_name=TunableLocalizedString( description= '\n Localized name of region.\n ', allow_none=True), region_description=TunableLocalizedString( description= '\n Localized description of region.\n ', allow_none=True), overlay_layer=TunableResourceKey( description= '\n Hero image displayed on mouse over of region in\n\t\t\t\t\t\tworld selection UI.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, allow_none=True), parallax_layers=TunableList( description= '\n Images used for scrolling parallax layers for region\n in world selection UI. Max number of images = 5.\n ', maxlength=5, tunable=TunableResourceKey( resource_types=sims4.resources.CompoundTypes.IMAGE )), is_destination_region=Tunable( description= '\n Whether this region is a destination world.\n ', tunable_type=bool, default=False))), promo_cycle_images=TunableList( description= '\n A list of promo screenshots and titles to display in the \n Pack Detail panel.\n ', tunable=PromoCycleImagesTuning( description= '\n Screenshots and label displayed in the Pack Detail Panel\n and Pack Preview Panel.\n ' )), short_description=TunableLocalizedString( description= '\n Short description of the pack meant to be displayed in \n a tooltip.\n ', allow_none=True)), export_modes=(ExportModes.ClientBinary, ), tuple_name='PackSpecificDataTuple') BUNDLE_SPECIFIC_DATA = TunableMapping( description= '\n Mapping from an MTX Bundle to its associated data. This is for bundles that\n should appear in the ui, but are not packs. This includes main menu icons,\n description, and the action associated with that bundle.\n ', key_type=TunableMTXBundle( description= '\n The MTX bundle id for the associated data.\n ', pack_safe=True), value_type=TunableTuple( description= '\n Each bundle has icons and a description, as well as an\n data for the action performed when the bundle is interacted \n with either the PromotionDialog or the PackDisplayPanel.\n ', bundle_name=TunableLocalizedString( description= '\n Name used in pack detail panel and main menu. If empty,\n we fall back to using the MTX product name.\n ', allow_none=True), icon_owned=TunableIcon( description= '\n Bundle icon that is displayed in the main menu\n pack display when the player is entitled to that bundle.\n ' ), icon_unowned=TunableIcon( description= '\n Bundle icon that is displayed in the main menu\n pack display when the player is not entitled to that bundle.\n ' ), short_description=TunableLocalizedString( description= '\n Short description of the bundle meant to be displayed in \n a tooltip.\n ' ), action=TunableVariant( description= '\n The action that should be performed when this bundle is interacted with\n in either the PromotionDialog or the PackDisplayPanel.\n ', url=Tunable( description= '\n External url to open from PackDisplayPanel.\n ', tunable_type=str, default=None), promo_data=TunableTuple( description= '\n Data that populates PromotionDialog.\n ', title=TunableLocalizedString( description= '\n Title of the promotion.\n ' ), text=TunableLocalizedString( description= '\n Text describing the promotion.\n ' ), image=TunableIcon( description= '\n Image displayed in the promotion dialog.\n ' ), legal_text=TunableLocalizedString( description= '\n Legal text required for this promotion.\n ', allow_none=True), export_class_name='TunablePromoDataTuple'), default='url'), export_class_name='TunableBundleDataTuple'), export_modes=(ExportModes.ClientBinary, ), tuple_name='BundleSpecificDataTuple') PACK_RELEASE_ORDER = TunableList( description='\n List of Pack Ids in release order.\n ', tunable=TunableEnumEntry( description='\n A pack Id.\n ', tunable_type=Pack, default=Pack.BASE_GAME), export_modes=(ExportModes.ClientBinary, )) CHALLENGE_DATA = TunableList( description= '\n List of challenge event data for engagement challenge notification UI.\n ', tunable=TunableTuple( description= '\n Data for each engagement challenge event.\n ', export_class_name='TunableChallengeNotificationTuple', challenge_list=TunableList( description= '\n A list of tuples that describe each challenge.\n ', tunable=TunableTuple( description= '\n A tuple that contains data for a challenge.\n ', export_class_name='TunableChallengeDataTuple', challenge_description=TunableLocalizedString( description= '\n The description of the challenge.\n ', allow_none=True), challenge_name=TunableLocalizedString( description= '\n The name of the challenge.\n ', allow_none=True), image=TunableResourceKey( description= '\n The main image displayed for challenge info.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), info_link=Tunable( description= '\n The url link to page for more info on a challenge.\n ', tunable_type=str, default='', allow_empty=True), event_display=TunableTuple( description= '\n Display data for a challenge event.\n ', export_class_name='TunableChallengeEventDisplayTuple', event_icon=TunableIcon( description= '\n An icon to use for the challenge event.\n ', allow_none=True), event_title=TunableLocalizedString( description= '\n Title to display. If not provided, challenge name will be used.\n ', allow_none=True), end_time=TunableTuple( description= '\n Date and time (UTC) for when the challenge event is expected to end.\n This is currently used to compute the time remaining in the UI.\n ', display_name='End Time (UTC)', export_class_name='TunableChallengeDateTuple', year=TunableRange( description= '\n Year\n ', tunable_type=int, default=2016, minimum=2014), month=TunableRange( description= '\n Month\n ', tunable_type=int, default=1, minimum=1, maximum=12), day=TunableRange( description= '\n Day\n ', tunable_type=int, default=1, minimum=1, maximum=31), hour=TunableRange( description= '\n Hour (24-hour)\n ', tunable_type=int, default=0, minimum=0, maximum=23), minute=TunableRange( description= '\n Minute\n ', tunable_type=int, default=0, minimum=0, maximum=59)), activity_icon=TunableIcon( description= '\n Icon to display beside the activity progress bar.\n ', allow_none=True), activity_progress_text=TunableLocalizedString( description= "\n Status text for when the player is still making progress towards\n the challenge goal. This is currently displayed on a tooltip.\n A CSS class of 'timeremaining' will have its color changed\n when the event is close to ending.\n The following tokens are available:\n 0 - Number: Current collection progress, if available.\n 1 - Number: Collection goal, if available.\n 2 - Number: Hours remaining.\n 3 - Number: Days remaining.\n ", allow_none=True), activity_progress_icon=TunableIcon( description= '\n Icon to be paired with the progress text.\n ', allow_none=True), activity_complete_text=TunableLocalizedString( description= "\n Status text for when the player has met the challenge goal.\n This is currently displayed on a tooltip.\n If not specified, the in-progress text will be used.\n A CSS class of 'timeremaining' will have its color changed\n when the event is close to ending.\n The following tokens are available (same as the in-progress text):\n 0 - Number: Current collection progress, if available.\n 1 - Number: Collection goal, if available.\n 2 - Number: Hours remaining.\n 3 - Number: Days remaining.\n ", allow_none=True), activity_complete_icon=TunableIcon( description= '\n Icon to be paired with the challenge complete text.\n If not specified, the in-progress icon will be used.\n ', allow_none=True), community_progress_text=TunableLocalizedString( description= "\n Status text describing the community's progress.\n This is currently displayed on a tooltip.\n This text is displayed even when challenges do not have\n community goals.\n Two Number tokens are available:\n 0 - Current community collection progress.\n 1 - Community collection goal, if any.\n ", allow_none=True), community_progress_icon=TunableIcon( description= '\n Icon to be paired with the community status text.\n ', allow_none=True), community_complete_text=TunableLocalizedString( description= '\n Status text for when the community has met the challenge goal.\n This text is only used when a goal is defined.\n If not specified, the in-progress status text will be used.\n Two Number tokens are available:\n 0 - Current community collection progress.\n 1 - Community collection goal, if any.\n ', allow_none=True), community_complete_icon=TunableIcon( description= '\n Icon to be paired with the community challenge complete text.\n ', allow_none=True), community_goal_amount=Tunable( description= '\n Optional collection goal for the community to reach.\n ', tunable_type=int, default=0)), collection_id=TunableEnumEntry( description= "\n A CollectionIdentifier that is associated with this\n challenge. This is used by the UI to tie a collectible \n with this challenge.\n \n Use the default of Unindentified for challenges that\n aren't associated with a particular collection.\n ", tunable_type=CollectionIdentifier, default=CollectionIdentifier.Unindentified, export_modes=ExportModes.All), reward_items=TunableList( description= '\n A list of tuples that describe rewards for challenge.\n ', tunable=TunableTuple( description= '\n A tuple that contains data for a challenge reward item.\n ', export_class_name='TunableChallengeRewardTuple', reward_icon=TunableResourceKey( description= '\n The icon of reward item.\n ', resource_types=sims4.resources.CompoundTypes. IMAGE), reward_name=TunableLocalizedString( description= '\n The name of reward item.\n ', allow_none=True))))), challenge_subtitle=TunableLocalizedString( description= '\n The subtitle text to be displayed in notification UI.\n ', allow_none=True), challenge_title=TunableLocalizedString( description= '\n The title text to be displayed in notification UI.\n ', allow_none=True), switch_name=Tunable( description= '\n Server switch name to check whether challenge is active.\n ', tunable_type=str, default='', allow_empty=True)), export_modes=(ExportModes.ClientBinary, )) PLATFORM_STRING_REPLACEMENTS = TunableList( description= '\n A list of strings that will be swapped out when in use on different \n platforms. Each entry contains the original and replacement LocKey, the platforms\n to perform the swap on, and the input method that is in use when the\n LocKey is used.\n ', tunable=TunableTuple( original_string=TunableLocalizedString( description= '\n The string that will be replaced or ignored.\n ' ), replacement_string=OptionalTunable( description= '\n The string that will be used in place of original_string. If\n omitted, original_string will simply be ignored entirely.\n ', tunable=TunableLocalizedString()), platform=TunableEnumEntry( description= '\n The platforms on which the string will be replaced.\n ', tunable_type=Platform, default=Platform.CONSOLE), input_method=TunableEnumEntry( description= '\n The input method that should be in use when attempting to replace\n the original_string.\n ', tunable_type=InputMethod, default=InputMethod.ANY), export_modes=ExportModes.ClientBinary, export_class_name='PlatformStringReplacementTuple')) SCALING = TunableList( description= '\n Defines a min/max ui scaling value for a screen resolution.\n ', tunable=TunableTuple( screen_width=Tunable( description= '\n Provide an integer value.\n ', tunable_type=int, default=0), screen_height=Tunable( description= '\n Provide an integer value.\n ', tunable_type=int, default=0), scale_max=Tunable( description= '\n Provide a float value.\n ', tunable_type=float, default=1), scale_min=Tunable( description= '\n Provide a float value.\n ', tunable_type=float, default=1), export_modes=ExportModes.ClientBinary, export_class_name='UIScaleTuple')) CG_CHALLENGE_DATAS = TunableList( description="\n A list of a challenge's data.\n ", tunable=TunableTuple( cg_challenge_hashtag=TunableLocalizedString( description= '\n Hashtag of this challenge\n ' ), cg_challenge_name=TunableLocalizedString( description= '\n Name of this challenge\n '), export_modes=ExportModes.ClientBinary, export_class_name='CGChallengeTuning')) DEFAULT_OVERLAY_MAP = TunableMapping( description= '\n This is a mapping of MapOverlayEnum -> List of MapOverlayEnums. The key\n is used as the layer to be shown when no other overlays are present.\n The value is a list of overlay types that would result in the default\n layer being turned off if both are active.\n ', key_type=TunableEnumEntry( description= '\n This is the OverlayType that acts as the default for the grouping\n of OverlayTypes.\n ', tunable_type=MapOverlayEnum, default=MapOverlayEnum.NONE), value_type=TunableList( description= '\n A list of OverlayTypes, that if turned on would result in the\n default OverlayType being shut off.\n ', tunable=TunableEnumEntry( description= '\n The OverlayType that causes the default value to turn off.\n ', tunable_type=MapOverlayEnum, default=MapOverlayEnum.NONE)), export_modes=ExportModes.All, tuple_name='OverlayDefaultData')
class CasStoriesQuestion(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.CAS_STORIES_QUESTION)): INSTANCE_TUNABLES = {'text': TunableLocalizedStringFactory(description='\n The text of this question.\n ', export_modes=ExportModes.ClientBinary, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n The icon for this question.\n ', export_modes=ExportModes.ClientBinary, tuning_group=GroupNames.UI), 'possible_answers': TunableList(description='\n Answers to this question.\n ', tunable=CasStoriesAnswer.TunableReference(), export_modes=ExportModes.ClientBinary), 'pack': TunableEnumEntry(description='\n The pack with which this question is associated. Will affect when\n and how frequently this answer is selected by CAS Stories.\n ', tunable_type=Pack, default=Pack.BASE_GAME, export_modes=ExportModes.ClientBinary)}
class UniversityMajor(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR)): INSTANCE_TUNABLES = {'courses': TunableList(description='\n List of courses, in order, for this major\n ', tunable=UniversityCourseData.TunableReference(), minlength=1), 'acceptance_score': TunableTuple(description='\n Score requirement to be accepted in this major as prestige degree.\n ', score=TunableMultiplier.TunableFactory(description='\n Define the base score and multiplier to calculate acceptance\n score of a Sim.\n '), score_threshold=TunableThreshold(description='\n The threshold to perform against the score to see if a Sim \n can be accepted in this major.\n ')), 'display_name': TunableLocalizedString(description="\n The major's name.\n ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'display_description': TunableLocalizedString(description="\n The major's description.\n ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icons': TunableTuple(description='\n Display icons for this major.\n ', icon=TunableIcon(description="\n The major's icon.\n "), icon_prestige=TunableIcon(description="\n The major's prestige icon.\n "), icon_high_res=TunableIcon(description="\n The major's high resolution icon.\n "), icon_prestige_high_res=TunableIcon(description="\n The major's prestige high resolution icon.\n "), export_class_name='UniversityMajorIconTuple', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'major_benefit_map': TunableMapping(description='\n University specific major benefit description. Each university can \n have its own description defined for this University Major.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableLocalizedString(description='\n Major benefit description.\n '), tuple_name='UniversityMajorBenefitMapping', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'graduation_reward': TunableMapping(description='\n Loot on graduation at each university for each GPA threshold\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableList(description='\n Loot for each GPA range (lower bound inclusive, upper bound\n exclusive.\n ', tunable=TunableTuple(gpa_range=TunableInterval(description='\n GPA range to receive this loot.\n Lower bound inclusive, upper bound exclusive.\n ', tunable_type=float, default_lower=0, default_upper=10), loot=TunableList(tunable=LootActions.TunableReference(description='\n The loot action applied.\n ', pack_safe=True))))), 'career_tracks': TunableList(description='\n List of career tracks for which the UI will indicate this major\n will provide benefit. Is not used to actually provide said benefit.\n ', tunable=TunableReference(description='\n These are the career tracks that will benefit from this major.\n ', manager=services.get_instance_manager(sims4.resources.Types.CAREER_TRACK), pack_safe=True), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary, unique_entries=True)} @classmethod def graduate(cls, sim_info, university, gpa): resolver = SingleSimResolver(sim_info) if university in cls.graduation_reward: for grad_reward in cls.graduation_reward[university]: if grad_reward.gpa_range.lower_bound <= gpa < grad_reward.gpa_range.upper_bound: for loot_action in grad_reward.loot: loot_action.apply_to_resolver(resolver) @classmethod def get_sim_acceptance_score(cls, sim_info): resolver = SingleSimResolver(sim_info) return cls.acceptance_score.score.get_multiplier(resolver) @classmethod def can_sim_be_accepted(cls, sim_info): sim_score = cls.get_sim_acceptance_score(sim_info) return cls.acceptance_score.score_threshold.compare(sim_score)
class _UniversityDynamicSignView(HasTunableSingletonFactory, AutoFactoryInit): class _LiteralString(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'text': TunableLocalizedStringFactory( description= '\n The text to be shown.\n \n * Token 0: Sim\n ' ) } def get_string(self, sim_info): return self.text(sim_info) class _FromSimInfo(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'university': TunableReference( description= '\n The university to get the data from.\n ', manager=services.get_instance_manager( sims4.resources.Types.UNIVERSITY)), 'info_type': TunableEnumEntry( description= '\n The type of the university info.\n ', tunable_type=UniversityInfoType, default=UniversityInfoType.INVALID, invalid_enums=(UniversityInfoType.INVALID, UniversityInfoType.ORGANIZATIONS)), 'fallback_string': TunableLocalizedStringFactory( description= '\n The string to be shown when failing to find string from\n the tuned sim degree info.\n \n * Token 0: Sim\n ' ) } def get_string(self, sim_info): degree_tracker = sim_info.degree_tracker if degree_tracker is None: logger.error( 'Trying to perform UniversityDynamicSignView op on sim {} with no degree tracker.', sim_info) return uni = self.university manager = services.get_instance_manager( sims4.resources.Types.UNIVERSITY_MAJOR) degree_ids = degree_tracker.get_available_degrees_to_enroll()[ uni.guid64] bullet_points = () if self.info_type == UniversityInfoType.PRESTIGE_DEGREES: bullet_points = (manager.get(i).display_name for i in degree_ids if i in uni.prestige_degree_ids) elif self.info_type == UniversityInfoType.NON_PRESTIGE_DEGREES: bullet_points = (manager.get(i).display_name for i in degree_ids if i in uni.non_prestige_degree_ids) final_string = LocalizationHelperTuning.get_bulleted_list( None, *bullet_points) if final_string is None: final_string = self.fallback_string(sim_info) return final_string class _FromUniversityInfo(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'university': TunableReference( description= '\n The university to get the data from.\n ', manager=services.get_instance_manager( sims4.resources.Types.UNIVERSITY)), 'info_type': TunableEnumEntry( description= '\n The type of the university info.\n ', tunable_type=UniversityInfoType, default=UniversityInfoType.INVALID, invalid_enums=(UniversityInfoType.INVALID, )) } def get_string(self, _): bullet_points = () if self.info_type == UniversityInfoType.PRESTIGE_DEGREES: bullet_points = (d.display_name for d in self.university.prestige_degrees) elif self.info_type == UniversityInfoType.NON_PRESTIGE_DEGREES: bullet_points = ( d.display_name for d in self.university.non_prestige_degrees) elif self.info_type == UniversityInfoType.ORGANIZATIONS: bullet_points = (o.display_name() for o in self.university.organizations if not o.hidden) return LocalizationHelperTuning.get_bulleted_list( None, *bullet_points) FACTORY_TUNABLES = { 'title': TunableLocalizedString( description= '\n The title to be shown on top of view.\n ' ), 'display_image': TunableResourceKey( description= '\n The image for this view display.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), 'background_image': TunableResourceKey( description= '\n The background image for this view display.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), 'sub_infos': TunableList( description= '\n The sub info to be shown on the bottom of the view.\n ', tunable=TunableTuple( description= '\n A single info.\n ', name=TunableLocalizedString( description= '\n The name of this info.\n ' ), desc=TunableVariant( description= '\n The description of this info.\n ', literal=_LiteralString.TunableFactory(), from_sim_info=_FromSimInfo.TunableFactory(), from_university_info=_FromUniversityInfo. TunableFactory(), default='literal'), icon=TunableIcon( description= '\n The Icon that represents this info.\n ' ))) } def perform(self, subject, target, resolver): if subject is None: logger.error( 'Trying to perform UniversityDynamicSignView op but subject is None. Resolver {}.', resolver) return if not subject.is_sim: logger.error( 'Trying to perform UniversityDynamicSignView op but subject {} is not Sim. Resolver {}.', subject, resolver) return sign_info = UI_pb2.DynamicSignView() sign_info.name = self.title sign_info.image = sims4.resources.get_protobuff_for_key( self.display_image) sign_info.background_image = sims4.resources.get_protobuff_for_key( self.background_image) for sub_info in self.sub_infos: with ProtocolBufferRollback( sign_info.activities) as activity_msg: activity_msg.name = sub_info.name activity_msg.description = sub_info.desc.get_string( subject.sim_info) activity_msg.icon = create_icon_info_msg( IconInfoData(sub_info.icon)) distributor = Distributor.instance() distributor.add_op( subject.sim_info, GenericProtocolBufferOp(Operation.DYNAMIC_SIGN_VIEW, sign_info))
def get_display_mixin(has_description=False, has_icon=False, has_tooltip=False, use_string_tokens=False, export_modes=(), enabled_by_default=False): tunable_localized_string_type = TunableLocalizedStringFactory if use_string_tokens else TunableLocalizedString export_to_client = ExportModes.ClientBinary in export_modes display_properties = { 'instance_display_name': OptionalTunable( description= '\n If enabled, specify a display name for this instance.\n ', tunable=tunable_localized_string_type( description= "\n The instance's name.\n "), enabled_by_default=True, export_modes=export_modes, enabled_name='enabled_display_name' if export_to_client else 'enabled') } if has_description: display_properties['instance_display_description'] = OptionalTunable( description= '\n If enabled, specify a display description for this instance.\n ', tunable=tunable_localized_string_type( description= "\n The instance's description. \n " ), enabled_by_default=True, export_modes=export_modes, enabled_name='enabled_display_description' if export_to_client else 'enabled') if has_icon: display_properties['instance_display_icon'] = OptionalTunable( description= '\n If enabled, specify a display icon for this instance.\n ', tunable=TunableIcon( description= "\n The instance's icon.\n "), enabled_by_default=True, export_modes=export_modes, enabled_name='enabled_display_icon' if export_to_client else 'enabled') if has_tooltip: display_properties['instance_display_tooltip'] = OptionalTunable( description= '\n If enabled, specify a display tooltip for this instance.\n ', tunable=tunable_localized_string_type( description= "\n The instance's tooltip. \n " ), enabled_by_default=True, export_modes=export_modes, enabled_name='enabled_display_tooltip' if export_to_client else 'enabled') class _HasOptionalDisplayMixin: INSTANCE_TUNABLES = { '_display_data': OptionalTunable( description= '\n If enabled, specify display data for this instance.\n ', tunable=TunableTuple( description= "\n The instance's display data.\n ", export_class_name='OptionalDisplayMixinTunable' if export_to_client else 'TunableTuple', **display_properties), tuning_group=GroupNames.UI, export_modes=export_modes, enabled_name='optional_display_mixin' if export_to_client else 'enabled', enabled_by_default=enabled_by_default) } TUNING_FIELD_PREFIX = 'instance_' for display_property_name in display_properties: if display_property_name.startswith(TUNING_FIELD_PREFIX): property_name = display_property_name[len(TUNING_FIELD_PREFIX):] else: property_name = display_property_name setattr( _HasOptionalDisplayMixin, property_name, classproperty(lambda c, attr_name=display_property_name: getattr( c._display_data, attr_name) if c._display_data is not None else None)) return _HasOptionalDisplayMixin
class UseMusicProductionStationSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { 'music_track_data_snippet': TunableMusicTrackDataSnippet( description= '\n The reference to looping and fixed-length .propx files for the associated\n music track.\n ' ), 'channels': TunableMapping( description= '\n A map of channel enums and their associated data. \n ', key_type=TunableEnumEntry( description= '\n The enum for a channel.\n ', tunable_type=ChannelFlags, default=ChannelFlags.CHANNEL1), value_type=TunableTuple( description= '\n Channel specific data.\n ', channel_name=TunableLocalizedString( description= '\n The name to display for this channel. \n ' ), channel_tests=TunableTestSet( description= "\n The tests to display this channel's remix mixer\n " ))), 'turn_on_channel_display_name': TunableLocalizedStringFactory( description= '\n The name to display for remix mixers that turn on a channel.\n ' ), 'turn_on_channel_icon': TunableIcon( description= '\n The icon to display in the pie menu for remix mixers that turn on a channel. \n ', tuning_group=GroupNames.UI), 'turn_off_channel_display_name': TunableLocalizedStringFactory( description= '\n The name to display for remix mixers that turn off a channel. \n ' ), 'turn_off_channel_icon': TunableIcon( description= '\n The icon to display in the pie menu for remix mixers that turn off a channel. \n ', tuning_group=GroupNames.UI), 'audio_start_event': Tunable( description= '\n The script event to listen for from animation so we know when to\n start the music.\n ', tunable_type=int, default=520), 'audio_stop_event': Tunable( description= '\n The script event to listen for from animation so we know when to\n stop the music.\n ', tunable_type=int, default=521) } def __init__(self, aop, context, *args, **kwargs): super().__init__(aop, context, *args, exit_functions=(), force_inertial=False, additional_post_run_autonomy_commodities=None, **kwargs) self._sound = None self._stored_audio_component = None def build_basic_content(self, sequence=(), **kwargs): self.store_event_handler(self._play_music_track, self.audio_start_event) self.store_event_handler(self._stop_music_track, self.audio_stop_event) sequence = super().build_basic_content(sequence, **kwargs) return build_critical_section_with_finally(sequence, self._stop_music_track) def _play_music_track(self, event_data, *args, **kwargs): self._stored_audio_component = self.target.get_component( STORED_AUDIO_COMPONENT) if self._stored_audio_component is None: logger.error( '{} has no Stored Audio Component, which UseMusicProductionStationSuperInteraction requires for proper use.', self.target) return if self._sound is None: self._stored_audio_component.store_track( music_track_snippet=self.music_track_data_snippet) self._sound = self._stored_audio_component.play_looping_music_track( self.target) def _stop_music_track(self, event_data, *args, **kwargs): if self._sound is not None: self._sound.stop() self._sound = None def _exited_pipeline(self, *args, **kwargs): if self._stored_audio_component is not None: self._stored_audio_component.clear() self._stored_audio_component = None super()._exited_pipeline(*args, **kwargs)
class AgingData(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'ages': TunableMapping( description= '\n All available ages for this Sim, and any data associated with that\n specific age.\n ', key_type=TunableEnumEntry( description= '\n The available age for the Sim.\n ', tunable_type=Age, default=Age.ADULT, binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableTuple( description= '\n Any further data associated with this age.\n ', transition=TunableAgingTransitionReference( description= '\n The transition data associated with this age, such as\n dialogs, notifications, durations, etc...\n ', pack_safe=True), personality_trait_count=TunableRange( description= '\n The number of traits available to a Sim of this age.\n ', tunable_type=int, default=3, minimum=0, export_modes=ExportModes.All), cas_icon=TunableIcon( description= '\n Icon to be displayed in the ui for the age.\n ', export_modes=ExportModes.ClientBinary), cas_icon_selected=TunableIcon( description= '\n Icon to be displayed in the UI for the age when buttons are\n selected.\n ', export_modes=ExportModes.ClientBinary), cas_name=TunableLocalizedStringFactory( description= '\n The name to be displayed in the UI for the age.\n ', export_modes=ExportModes.ClientBinary), export_class_name='AvailableAgeDataTuple'), minlength=1, tuple_name='AvailableAgeDataMapping'), 'age_up_interaction': TunableReference( description= '\n The default interaction that ages Sims up. This is called when Sims\n auto-age or when the "Age Up" cheat is invoked.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True), 'old_age_interaction': TunableReference( description= '\n The default interaction that transitions a Sim from old age to\n death.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True), 'old_age_npc_death_type_fallback': TunableEnumEntry( description= "\n Used if the Old Age Interaction is not a death interaction. In that\n case, the non-instanced NPCs are not running the interaction but also\n can't get their death type from the interaction's tuning. This value\n is used as a fallback. The NPC's death type set to this value, and \n it will effectively become a ghost.\n ", tunable_type=DeathType, default=DeathType.NONE, pack_safe=True), 'bonus_days': TunableMapping( description= '\n Specify how much bonus time is added to elder Sims\n possessing these traits.\n ', key_type=TunableReference( description= '\n The trait associated with this modifier.\n ', manager=services.get_instance_manager( sims4.resources.Types.TRAIT), pack_safe=True), value_type=Tunable( description= '\n The modifier associated with this trait.\n ', tunable_type=float, default=0)) } def get_age_transition_data(self, age): return self.ages[age].transition def get_birth_age(self): return min(self.ages) def get_lifetime_duration(self, sim_info): total_lifetime = sum( age_data.transition.get_age_duration(sim_info) for age_data in self.ages.values()) aging_service = services.get_aging_service() total_lifetime /= aging_service.get_speed_multiple( aging_service.aging_speed) return total_lifetime def get_lifetime_bonus(self, sim_info): lifetime_duration = self.get_lifetime_duration(sim_info) bonus_multiplier = sum(modifier for (trait, modifier) in self.bonus_days.items() if sim_info.has_trait(trait)) return lifetime_duration * bonus_multiplier def get_personality_trait_count(self, age): age_data = self.ages.get(age, None) if age_data is None: raise ValueError('{} is not in {}'.format(age, self.ages)) return age_data.personality_trait_count def get_next_age(self, age): ages = tuple(sorted(self.ages)) for (current_age, next_age) in zip(ages, ages[1:]): if current_age <= age < next_age: return next_age raise ValueError('There is no age after {}'.format(age)) def get_previous_age(self, age): ages = tuple(sorted(self.ages)) for (previous_age, current_age) in zip(ages, ages[1:]): if previous_age < age <= current_age: return previous_age
def __init__(self, description='A grouping of headline update data.', **kwargs): super().__init__(description=description, icon=TunableIcon(description='\n The icon that we will use for this update.\n '), minimum_value=Tunable(description='\n The minimum value that this update level will be used.\n ', tunable_type=float, default=0.0), maximum_value=Tunable(description='\n The maximum value that this update level will be used.\n ', tunable_type=float, default=1.0), fx=TunableEnumEntry(description='\n The fx on the flash timeline that should be used.\n ', tunable_type=FXType, default=FXType.NO_EFFECT), **kwargs)
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 CASTuning: CAS_BRANDED_TAG_DATA = TunableList( description= '\n A list of CAS tag to data used to show a branded logo on the item\n ', tunable=TunableTuple( description= '\n Tuning for branded logo to use.\n ', tag=TunableTag( description= '\n Tag to use for the brand to be displayed\n ' ), icon=TunableIcon( description= '\n Icon to be displayed on the item\n ' ), background_type=TunableEnumEntry( description= '\n Background to be used for it\n ', tunable_type=CASBrandedLogoBackground, default=CASBrandedLogoBackground.LIGHT), export_class_name='CasBrandedTagEntry'), export_modes=ExportModes.ClientBinary) CAS_SPECIES_PAINT_POSES = TunableMapping( description= '\n A mapping of species type to data that is required for the paint pose ui\n ', key_type=TunableEnumEntry( description= '\n The species type that this entry applies to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID, )), value_type=TunableList( description= '\n A list of CasPaintPostTuples\n ', tunable=TunableTuple( description= '\n Data required for each UI Paint pose button.\n ', icon=TunableIcon( description= '\n Icon to be displayed on the button for the pose\n ', tuning_group=GroupNames.UI), icon_selected=TunableIcon( description= '\n Icon to be displayed on the button when the pose button is selected\n ', tuning_group=GroupNames.UI), pose=TunableEnumEntry( description= '\n The pose to play when the button is pressed\n ', tunable_type=CASPaintPose, default=CASPaintPose.NONE), export_class_name='CasPaintPoseTuple')), export_modes=ExportModes.ClientBinary, tuple_name='CasPaintPoseKeyValue') CAS_VOICES_DATA = TunableMapping( description= '\n A mapping of species type to data required for the personality panel ui.\n ', key_type=TunableEnumEntry( description= '\n The species type that this entry applies to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID, )), value_type=TunableMapping( description= '\n A mapping of age type to data required for displaying voices in the ui.\n ', key_type=TunableEnumEntry( description= '\n The age that this entry applies to.\n ', tunable_type=Age, default=Age.ADULT), value_type=TunableList( description= '\n a list of voice data for this species at this age.\n ', tunable=TunableTuple( description= '\n data required to display this voice in the ui.\n ', icon=TunableIcon( description= '\n Icon to be displayed as voice button.\n ', tuning_group=GroupNames.UI), icon_selected=TunableIcon( description= '\n Icon to be displayed as voice button when it is selected.\n ', tuning_group=GroupNames.UI), tooltip=TunableLocalizedString( description= '\n Localized name of this voice.\n ' ), export_class_name='CasVoicesDataTuple')), tuple_name='CasVoicesAgeKeyValue'), export_modes=ExportModes.ClientBinary, tuple_name='CasVoicesSpeciesKeyValue') CAS_RANDOMIZE_FILTERS = TunableMapping( description= '\n An Ordered list of randomization menu items that will appear in the randomization panel ui in CAS. \n The list is filtered by the criteria on each menu item.\n ', key_type=Tunable( description= '\n An integer value used to set the specific order of the menu items\n in the ui. The lower numbers are displayed first in the ui.\n ', tunable_type=int, default=0), value_type=TunableTuple( description= '\n A randomization menu item and its inclusion (and/or exclusion) criteria.\n ', criteria=CASContextCriterionList( description= '\n Use this menu item if all of the specified criteria are met.\n ' ), flags=TunableList( description= '\n A list of randomization flags for this item.\n ', tunable=TunableEnumEntry( description= '\n A randomization flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), name=TunableLocalizedString( description= '\n The name of this menu item displayed in the ui.\n ' ), required_flags=TunableList( description= '\n A list of randomization flags that are required to be enabled \n in order for this menu item to be enabled. \n ', tunable=TunableEnumEntry( description= '\n A randomization flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), export_class_name='CasRandomizeItemTuple'), tuple_name='CasRandomizeItemsKeyValue', export_modes=(ExportModes.ClientBinary, )) CAS_COPY_FILTERS = TunableList( description= '\n An Ordered list of copy menu items that will appear in the randomization panel ui in CAS. \n The list is filtered by the criteria on each menu item.\n ', tunable=TunableTuple( description= '\n A copy menu item and its inclusion (and/or exclusion) criteria.\n ', criteria=CASContextCriterionList( description= '\n Use this menu item if all of the specified criteria are met.\n ' ), flags=TunableList( description= '\n A list of copy flags for this item.\n ', tunable=TunableEnumEntry( description= '\n A copy flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), name=TunableLocalizedString( description= '\n The name of this menu item displayed in the ui.\n ' ), required_flags=TunableList( description= '\n A list of copy flags that are required to be enabled \n in order for this menu item to be enabled. \n ', tunable=TunableEnumEntry( description= '\n A copy flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), export_class_name='CasCopyItemEntry'), export_modes=(ExportModes.ClientBinary, )) CAS_ADD_SIM_MENU_DATA = TunableMapping( description= '\n An ordered mapping of menu data used for the Add Sim portion of CAS.\n ', key_name='index', key_type=Tunable( description= '\n The order in which these entries should be added. 1 is first, 2 is\n second, etc.\n ', tunable_type=int, default=0), value_name='data', value_type=TunableTuple( description= '\n Data associated with an add Sim button in CAS.\n ', criteria=CASContextCriterionList( description= '\n Only add this menu item if the criteria are met.\n ' ), parent_index=Tunable( description= '\n The index of the parent entry if this is a child to another\n entry in the list. 0 if this entry has no parent.\n ', tunable_type=int, default=0), tooltip=TunableLocalizedString( description= '\n The tooltip when hovering over this entry.\n ', allow_none=True), icon=TunableResourceKey( description= '\n The icon for this entry.\n ', allow_none=True, pack_safe=True), icon_selected=TunableResourceKey( description= '\n The icon when this entry is selected.\n ', allow_none=True, pack_safe=True), audio_name=Tunable( description= '\n The audio to play when this entry is selected.\n ', tunable_type=str, default='', allow_empty=True), flair_name=Tunable( description= '\n Flair to apply to this entry (for instance, god rays).\n ', tunable_type=str, default='', allow_empty=True), tutorial_control_enum=TunableEnumEntry( description= '\n The enum used for tutorial controls. UI_INVALID should be\n used if this entry has no tutorial control.\n ', tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), action=TunableEnumEntry( description= '\n The action to take when clicking this entry.\n ', tunable_type=CASAddSimAction, default=CASAddSimAction.ACTION_NONE), species=TunableEnumEntry( description= "\n The species for this entry. Species.INVALID indicates no\n preference or it's not relevant to this menu entry.\n ", tunable_type=SpeciesExtended, default=SpeciesExtended.INVALID), occult_type=TunableEnumFlags( description= '\n The occult type for this entry, if any.\n ', enum_type=OccultType, allow_no_flags=True), limit_genetics_species=TunableEnumSet( description= '\n Species in this list will only be allowed through if the action\n for this entry is GENETICS. This is very likely only going to be\n used for pet genetics.\n ', enum_type=SpeciesExtended, enum_default=SpeciesExtended.INVALID, allow_empty_set=True), export_class_name='CasAddSimMenuData'), tuple_name='CasAddSimMenuDataKeyValue', export_modes=(ExportModes.ClientBinary, ))
class Business(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.BUSINESS)): INSTANCE_TUNABLES = { 'employee_data_map': TunableMapping( description= '\n The mapping between Business Employee Type and the Employee Data for\n that type.\n ', key_type=TunableEnumEntry( description= '\n The Business Employee Type that should get the specified Career.\n ', tunable_type=BusinessEmployeeType, default=BusinessEmployeeType.INVALID, invalid_enums=(BusinessEmployeeType.INVALID, )), value_type=TunableBusinessEmployeeDataSnippet( description= '\n The Employee Data for the given Business Employee Type.\n ' ), tuning_group=GroupNames.EMPLOYEES), 'npc_starting_funds': TunableRange( description= '\n The amount of money an npc-owned store will start with in their\n funds. Typically should be set to the same cost as the interaction\n to buy the business.\n ', tunable_type=int, default=0, minimum=0, tuning_group=GroupNames.CURRENCY), 'funds_category_data': TunableMapping( description= '\n Data associated with specific business funds categories.\n ', key_type=TunableEnumEntry( description= '\n The funds category.\n ', tunable_type=BusinessFundsCategory, default=BusinessFundsCategory.NONE, invalid_enums=(BusinessFundsCategory.NONE, )), value_type=TunableTuple( description= '\n The data associated with this retail funds category.\n ', summary_dialog_entry=OptionalTunable( description= "\n If enabled, an entry for this category is displayed in the\n business' summary dialog.\n ", tunable=TunableLocalizedString( description= '\n The dialog entry for this retail funds category. This\n string takes no tokens.\n ' ))), tuning_group=GroupNames.CURRENCY), 'default_markup_multiplier': TunableRange( description= '\n The default markup multiplier for a new business. This must match a\n multiplier that\'s in the Markup Multiplier Data tunable. It\'s also\n possible for this to be less than 1, meaning the default "markup"\n will actually cause prices to be lower than normal.\n ', tunable_type=float, default=1.25, minimum=math.EPSILON, tuning_group=GroupNames.CURRENCY), 'advertising_name_map': TunableMapping( description= '\n The mapping between advertising enum and the name used in the UI for\n that type.\n ', key_name='advertising_enum', key_type=TunableEnumEntry( description= '\n The Advertising Type.\n ', tunable_type=BusinessAdvertisingType, default=BusinessAdvertisingType.INVALID, invalid_enums=(BusinessAdvertisingType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), value_name='advertising_name', value_type=TunableLocalizedString( description= '\n The name of the advertising type used in the UI.\n ' ), tuple_name='AdvertisingEnumDataMappingTuple', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'advertising_type_sort_order': TunableList( description= '\n Sort order for the advertising types in the UI\n ', tunable=TunableEnumEntry( description= '\n The Advertising Type.\n ', tunable_type=BusinessAdvertisingType, default=BusinessAdvertisingType.INVALID, invalid_enums=(BusinessAdvertisingType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), unique_entries=True, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'quality_settings': TunableList( description= '\n Tunable Business quality settings. \n \n Quality type can be interpreted in different ways \n by specific businesses, and can be used for tests.\n \n These are quality settings that are exported to the client.\n \n The order in this list should be the order we want them displayed\n in the UI.\n ', tunable=TunableTuple( quality_type=TunableEnumEntry( description= '\n The quality Type.\n ', tunable_type=BusinessQualityType, default=BusinessQualityType.INVALID, invalid_enums=(BusinessQualityType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), quality_name=TunableLocalizedString( description= '\n The name of the quality type used in the UI.\n ' ), export_class_name='QualitySettingsData'), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'show_settings_button': OptionalTunable( description= "\n If enabled, this business type will show the settings button with \n the tuned tooltip text. If disabled, this business type won't show\n the settings button.\n ", tunable=TunableLocalizedString( description= '\n The tooltip to show on the settings button when it is shown\n for this business type.\n ' ), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'business_summary_tooltip': OptionalTunable( description= '\n If enabled, allows tuning a business summary tooltip. If disabled, no\n tooltip will be used or displayed by the UI.\n ', tunable=TunableLocalizedString( description= '\n The tooltip to show on the business panel.\n ' ), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'show_sell_button': Tunable( description= "\n If checked, the sell button will be shown in the business panel if\n the business is on the active lot. If left unchecked, the sell button\n won't be shown on the business panel at all.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'show_employee_button': Tunable(description='\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'default_quality': OptionalTunable( description= '\n The default quality type for the business.', tunable=TunableEnumEntry( tunable_type=BusinessQualityType, default=BusinessQualityType.INVALID, invalid_enums=(BusinessQualityType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), disabled_value=BusinessQualityType.INVALID, tuning_group=GroupNames.BUSINESS), 'quality_unlock_perk': OptionalTunable( description= '\n Reference to a perk that, if unlocked, allow the player to adjust\n the quality type specific to this business.\n ', tunable=TunablePackSafeReference( manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), tuning_group=GroupNames.BUSINESS), 'advertising_configuration': AdvertisingConfiguration.TunableFactory( description= '\n Tunable Business advertising configuration.\n ', tuning_group=GroupNames.BUSINESS), 'markup_multiplier_data': TunableList( description= '\n A list of markup multiplier display names and the actual multiplier\n associated with that name. This is used for sending the markup\n information to the UI.\n ', tunable=TunableTuple( description= '\n A tuple of the markup multiplier display name and the actual\n multiplier associated with that display name.\n ', name=TunableLocalizedString( description= '\n The display name for this markup multiplier. e.g. a\n multiplier of 1.2 will have "20 %" tuned here.\n ' ), markup_multiplier=TunableRange( description= '\n The multiplier associated with this display name.\n ', tunable_type=float, default=1, minimum=math.EPSILON), export_class_name='MarkupMultiplierData'), tuning_group=GroupNames.CURRENCY, export_modes=ExportModes.All), 'star_rating_to_screen_slam_map': TunableMapping( description= '\n A mapping of star ratings to screen slams.\n Screen slams will be triggered when the rating increases to a new\n whole value.\n ', key_type=int, value_type=ui.screen_slam.TunableScreenSlamSnippet(), key_name='star_rating', value_name='screen_slam', tuning_group=GroupNames.BUSINESS), 'show_empolyee_skill_level_up_notification': Tunable( description= '\n If true, skill level up notifications will be shown for employees.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.EMPLOYEES), 'bucks': TunableEnumEntry( description= '\n The Bucks Type this business will use for Perk unlocks.\n ', tunable_type=BucksType, default=BucksType.INVALID, invalid_enums=(BucksType.INVALID, ), tuning_group=GroupNames.CURRENCY, export_modes=ExportModes.All), 'off_lot_star_rating_decay_multiplier_perk': OptionalTunable( description= '\n If enabled, allows the tuning of a perk which can adjust the off-lot star rating decay.\n ', tunable=TunableTuple( description= '\n The off lot star rating decay multiplier tuning.\n ', perk=TunableReference( description= '\n The perk that will cause the multiplier to be applied to the\n star rating decay during off-lot simulations.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), decay_multiplier=TunableRange( description= '\n If the household has the specified perk, the off-lot star\n rating decay rate will be multiplied by this value.\n ', tunable_type=float, default=1.1, minimum=0)), tuning_group=GroupNames.OFF_LOT), 'manage_outfit_affordances': TunableSet( description= '\n A list of affordances that are shown when the player clicks on the\n Manage Outfits button.\n ', tunable=TunableReference( description= '\n An affordance shown when the player clicks on the Manage Outfits\n button.\n ', manager=services.affordance_manager(), pack_safe=True), tuning_group=GroupNames.EMPLOYEES), 'employee_training_buff_tag': TunableEnumWithFilter( description= '\n A tag to indicate a buff is used for employee training.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, ), filter_prefixes=('buff', ), pack_safe=True, tuning_group=GroupNames.EMPLOYEES), 'customer_buffs_to_save_tag': TunableEnumWithFilter( description= '\n All buffs with this tag will be saved and reapplied to customer sims\n on load.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, ), filter_prefixes=('buff', ), pack_safe=True, tuning_group=GroupNames.CUSTOMER), 'customer_buffs_to_remove_tags': TunableSet( description= '\n Tags that indicate which buffs should be removed from customers when\n they leave the business.\n ', tunable=TunableEnumWithFilter( description= '\n A tag that indicates a buff should be removed from the customer\n when they leave the business.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, ), filter_prefixes=('buff', ), pack_safe=True), tuning_group=GroupNames.CUSTOMER), 'current_business_lot_transfer_dialog_entry': TunableLocalizedString( description= '\n This is the text that will show in the funds transfer dialog drop\n down for the current lot if it\'s a business lot. Typically, the lot\n name would show but if the active lot is a business lot it makes\n more sense to say something along the lines of\n "Current Retail Lot" or "Current Restaurant" instead of the name of the lot.\n ', tuning_group=GroupNames.UI), 'open_business_notification': TunableUiDialogNotificationSnippet( description= '\n The notification that shows up when the player opens the business.\n We need to trigger this from code because we need the notification\n to show up when we open the store through the UI or through an\n Interaction.\n ', tuning_group=GroupNames.UI), 'no_way_to_make_money_notification': TunableUiDialogNotificationSnippet( description= '\n The notification that shows up when the player opens a store that has no\n way of currently making money (e.g. retail store having no items set for\n sale or restaurants having nothing on the menu). It will replace the\n Open Business Notification.\n ', tuning_group=GroupNames.UI), 'audio_sting_open': TunablePlayAudioAllPacks( description= '\n The audio sting to play when the store opens.\n ', tuning_group=GroupNames.UI), 'audio_sting_close': TunablePlayAudioAllPacks( description= '\n The audio sting to play when the store closes.\n ', tuning_group=GroupNames.UI), 'sell_store_dialog': UiDialogOkCancel.TunableFactory( description= '\n This dialog is to confirm the sale of the business.\n ', tuning_group=GroupNames.UI), 'lighting_helper_open': LightingHelper.TunableFactory( description= '\n The lighting helper to execute when the store opens.\n e.g. Turn on all neon signs.\n ', tuning_group=GroupNames.TRIGGERS), 'lighting_helper_close': LightingHelper.TunableFactory( description= '\n The lighting helper to execute when the store closes.\n e.g. Turn off all neon signs.\n ', tuning_group=GroupNames.TRIGGERS), 'min_and_max_star_rating': TunableInterval( description= '\n The lower and upper bounds for a star rating. This affects both the\n customer star rating and the overall business star rating.\n ', tunable_type=float, default_lower=1, default_upper=5, tuning_group=GroupNames.BUSINESS), 'min_and_max_star_rating_value': TunableInterval( description= '\n The minimum and maximum star rating value for this business.\n ', tunable_type=float, default_lower=0, default_upper=100, tuning_group=GroupNames.BUSINESS), 'star_rating_value_to_user_facing_rating_curve': TunableCurve( description= '\n Curve that maps star rating values to the user-facing star rating.\n ', x_axis_name='Star Rating Value', y_axis_name='User-Facing Star Rating', tuning_group=GroupNames.BUSINESS), 'default_business_star_rating_value': TunableRange( description= '\n The star rating value a newly opened business will begin with. Keep in mind, this is not the actual star rating. This is the value which maps to a rating using \n ', tunable_type=float, default=1, minimum=0, tuning_group=GroupNames.BUSINESS), 'customer_rating_delta_to_business_star_rating_value_change_curve': TunableCurve( description= '\n When a customer is done with their meal, we will take the delta\n between their rating and the business rating and map that to an\n amount it should change the star rating value for the restaurant.\n \n For instance, the business has a current rating of 3 stars and the\n customer is giving a rating of 4.5 stars. 4.5 - 3 = a delta of 1.5.\n That 1.5 will map, on this curve, to the amount we should adjust the\n star rating value for the business.\n ', x_axis_name= 'Customer Rating to Business Rating Delta (restaurant rating - customer rating)', y_axis_name='Business Star Rating Value Change', tuning_group=GroupNames.BUSINESS), 'default_customer_star_rating': TunableRange( description= '\n The star rating a new customer starts with.\n ', tunable_type=float, default=3, minimum=0, tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_buff_bucket_data': TunableMapping( description= "\n A mapping from Business Customer Star Rating Buff Bucket to the data\n associated with the buff bucker for this business.\n \n Each buff bucket has a minimum, median, and maximum value. For every\n buff a customer has that falls within a buff bucket, that buff's\n Buff Bucket Delta is added to that bucket's totals. The totals are\n clamped between -1 and 1 and interpolated against the\n minimum/medium/maximum value for their associated buckets. All of\n the final values of the buckets are added together and that value is\n used in the Customer Star Buff Bucket To Rating Curve to determine\n the customer's final star rating of this business.\n \n For instance, assume a buff bucket has a minimum value of -200, median of 0,\n and maximum of 100, and the buff bucket's clamped total is 0.5, the actual\n value of that bucket will be 50 (half way, or 0.5, between 0 and\n 100). If, however, the bucket's total is -0.5, we'd interpolate\n between the bucket's minimum value, -200, and median value, 0, to arrive at a\n bucket value of -100.\n ", key_name='Star_Rating_Buff_Bucket', key_type=TunableEnumEntry( description= '\n The Business Customer Star Rating Buff Bucket enum.\n ', tunable_type=BusinessCustomerStarRatingBuffBuckets, default=BusinessCustomerStarRatingBuffBuckets.INVALID, invalid_enums=( BusinessCustomerStarRatingBuffBuckets.INVALID, )), value_name='Star_Rating_Buff_Bucket_Data', value_type=TunableTuple( description= '\n All of the data associated with a specific customer star rating\n buff bucket.\n ', bucket_value_minimum=Tunable( description= "\n The minimum value for this bucket's values.\n ", tunable_type=float, default=-100), positive_bucket_vfx=PlayEffect.TunableFactory( description= '\n The vfx to play when positive change star value occurs. \n ' ), negative_bucket_vfx=PlayEffect.TunableFactory( description= '\n The vfx to play when negative change star value occurs.\n ' ), bucket_value_median=Tunable( description= "\n The median/middle value for this bucket's values.\n ", tunable_type=float, default=0), bucket_value_maximum=Tunable( description= "\n The maximum value for this bucket's values.\n ", tunable_type=float, default=100), bucket_icon=TunableIconAllPacks( description= '\n The icon that represents this buff bucket.\n ' ), bucket_positive_text=TunableLocalizedStringFactoryVariant( description= '\n The possible text strings to show up when this bucket\n results in a positive star rating.\n ' ), bucket_negative_text=TunableLocalizedStringFactoryVariant( description= '\n The possible text strings to show up when this bucket\n results in a bad star rating.\n ' ), bucket_excellence_text=TunableLocalizedStringFactoryVariant( description= "\n The description text to use in the business summary panel if\n this buff bucket is in the 'Excellence' section.\n " ), bucket_growth_opportunity_text= TunableLocalizedStringFactoryVariant( description= "\n The description text to use in the business summary panel if\n this buff bucket is in the 'Growth Opportunity' section.\n " ), bucket_growth_opportunity_threshold=TunableRange( description= '\n The amount of score this bucket must be from the maximum to be\n considered a growth opportunity. \n ', tunable_type=float, minimum=0, default=10), bucket_excellence_threshold=TunableRange( description= '\n The amount of score this bucket must be before it is \n considered an excellent bucket\n ', tunable_type=float, minimum=0, default=1), bucket_title=TunableLocalizedString( description= '\n The name for this bucket.\n ' )), tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_buff_data': TunableMapping( description= '\n A mapping of Buff to the buff data associated with that buff.\n \n Refer to the description on Customer Star Rating Buff Bucket Data\n for a detailed explanation of how this tuning works.\n ', key_name='Buff', key_type=Buff.TunableReference( description= "\n A buff meant to drive a customer's star rating for a business.\n ", pack_safe=True), value_name='Buff Data', value_type=TunableTuple( description= '\n The customer star rating for this buff.\n ', buff_bucket=TunableEnumEntry( description= '\n The customer star rating buff bucket associated with this buff.\n ', tunable_type=BusinessCustomerStarRatingBuffBuckets, default=BusinessCustomerStarRatingBuffBuckets.INVALID, invalid_enums=( BusinessCustomerStarRatingBuffBuckets.INVALID, )), buff_bucket_delta=Tunable( description= '\n The amount of change this buff should contribute to its bucket.\n ', tunable_type=float, default=0), update_star_rating_on_add=Tunable( description= "\n If enabled, the customer's star rating will be re-\n calculated when this buff is added.\n ", tunable_type=bool, default=True), update_star_rating_on_remove=Tunable( description= "\n If enabled, the customer's star rating will be re-\n calculated when this buff is removed.\n ", tunable_type=bool, default=False)), tuning_group=GroupNames.CUSTOMER), 'customer_star_buff_bucket_to_rating_curve': TunableCurve( description= '\n A mapping of the sum of all buff buckets for a single customer to\n the star rating for that customer.\n \n Refer to the description on Customer Star Rating Buff Bucket Data\n for a detailed explanation of how this tuning works.\n ', x_axis_name='Buff Bucket Total', y_axis_name='Star Rating', tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_vfx_increase_arrow': OptionalTunable( description= '\n The "up arrow" VFX to play when a customer\'s star rating goes up.\n These will play even if the customer\'s rating doesn\'t go up enough\n to trigger a star change.\n ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_vfx_decrease_arrow': OptionalTunable( description= '\n The "down arrow" VFX to play when a customer\'s star rating goes\n down. These will play even if the customer\'s rating doesn\'t go down\n enough to trigger a star change.\n ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_vfx_mapping': TunableStarRatingVfxMapping( description= '\n Maps the star rating for the customer to the persistent star effect\n that shows over their head.\n ', tuning_group=GroupNames.CUSTOMER), 'customer_final_star_rating_vfx': OptionalTunable( description= '\n The VFX to play when the customer is done and is submitting their\n final star rating to the business.\n ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.CUSTOMER), 'customer_max_star_rating_vfx': OptionalTunable( description= '\n The VFX to play when the customer hits the maximum star rating.\n ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.CUSTOMER), 'customer_star_rating_statistic': TunablePackSafeReference( description= '\n The statistic on a customer Sim that represents their current star\n rating.\n ', manager=services.get_instance_manager(Types.STATISTIC), allow_none=True, tuning_group=GroupNames.CUSTOMER), 'buy_business_lot_affordance': TunableReference( description= '\n The affordance to buy a lot for this type of business.\n ', manager=services.get_instance_manager(Types.INTERACTION), tuning_group=GroupNames.UI), 'initial_funds_transfer_amount': TunableRange( description= '\n The amount to default the funds transfer dialog when a player\n initially buys this business.\n ', tunable_type=int, minimum=0, default=2500, tuning_group=GroupNames.CURRENCY), 'summary_dialog_icon': TunableIcon( description= '\n The Icon to show in the header of the dialog.\n ', tuning_group=GroupNames.UI), 'summary_dialog_subtitle': TunableLocalizedString( description= "\n The subtitle for the dialog. The main title will be the store's name.\n ", tuning_group=GroupNames.UI), 'summary_dialog_transactions_header': TunableLocalizedString( description= "\n The header for the 'Items Sold' line item. By design, this should say\n something along the lines of 'Items Sold:' or 'Transactions:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_transactions_text': TunableLocalizedStringFactory( description= "\n The text in the 'Items Sold' line item. By design, this should say\n the number of items sold.\n {0.Number} = number of items sold since the store was open\n i.e. {0.Number}\n ", tuning_group=GroupNames.UI), 'summary_dialog_cost_of_ingredients_header': TunableLocalizedString( description= "\n The header for the 'Cost of Ingredients' line item. By design, this\n should say something along the lines of 'Cost of Ingredients:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_cost_of_ingredients_text': TunableLocalizedStringFactory( description= "\n The text in the 'Cost of Ingredients' line item. {0.Number} = the\n amount of money spent on ingredients.\n ", tuning_group=GroupNames.UI), 'summary_dialog_food_profit_header': TunableLocalizedString( description= "\n The header for the 'Food Profits' line item. This line item is the\n total revenue minus the cost of ingredients. By design, this should\n say something along the lines of 'Food Profits:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_food_profit_text': TunableLocalizedStringFactory( description= "\n The text in the 'Food Profits' line item. {0.Number} = the amount of\n money made on food.\n ", tuning_group=GroupNames.UI), 'summary_dialog_wages_owed_header': TunableLocalizedString( description= "\n The header text for the 'Wages Owned' line item. By design, this\n should say 'Wages Owed:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_wages_owed_text': TunableLocalizedStringFactory( description= "\n The text in the 'Wages Owed' line item. By design, this should say the\n number of hours worked and the price per hour.\n {0.Number} = number of hours worked by all employees\n {1.Money} = amount employees get paid per hour\n i.e. {0.Number} hours worked x {1.Money}/hr\n ", tuning_group=GroupNames.UI), 'summary_dialog_payroll_header': TunableLocalizedStringFactory( description= '\n The header text for each unique Sim on payroll. This is provided one\n token, the Sim.\n ', tuning_group=GroupNames.UI), 'summary_dialog_payroll_text': TunableLocalizedStringFactory( description= '\n The text for each job that the Sim on payroll has held today. This is\n provided three tokens: the career level name, the career level salary,\n and the total hours worked.\n \n e.g.\n {0.String} ({1.Money}/hr) * {2.Number} {S2.hour}{P2.hours}\n ', tuning_group=GroupNames.UI), 'summary_dialog_wages_advertising_header': TunableLocalizedString( description= "\n The header text for the 'Advertising' line item. By design, this\n should say 'Advertising Spent:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_wages_advertising_text': TunableLocalizedStringFactory( description= "\n The text in the 'Advertising' line item. By design, this should say the\n amount spent on advertising\n ", tuning_group=GroupNames.UI), 'summary_dialog_wages_net_profit_header': TunableLocalizedString( description= "\n The header text for the 'Net Profit' line item. By design, this\n should say 'Net Profit:'\n ", tuning_group=GroupNames.UI), 'summary_dialog_wages_net_profit_text': TunableLocalizedStringFactory( description= "\n The text in the 'Net Profit' line item. By design, this should say the\n total amount earnt so far in this shift\n ", tuning_group=GroupNames.UI), 'grand_opening_notification': OptionalTunable( description= '\n If enabled, allows a notification to be tuned that will show only\n the first time you arrive on your business lot.\n ', tunable=TunableUiDialogNotificationSnippet(), tuning_group=GroupNames.UI), 'business_icon': TunableIcon( description= '\n The Icon to show in the header of the dialog.\n ', tuning_group=GroupNames.UI), 'star_rating_to_customer_count_curve': TunableCurve( description= '\n A curve mapping of current star rating of the restaurant to the base\n number of customers that should come per interval.\n ', x_axis_name='Star Rating', y_axis_name='Base Customer Count', tuning_group=GroupNames.CUSTOMER), 'time_of_day_to_customer_count_multiplier_curve': TunableCurve( description= '\n A curve that lets you tune a specific customer multiplier based on the \n time of day. \n \n Time of day should range between 0 and 23, 0 being midnight.\n ', tuning_group=GroupNames.CUSTOMER, x_axis_name='time_of_day', y_axis_name='customer_multiplier'), 'off_lot_customer_count_multiplier': TunableRange( description= '\n This value will be multiplied by the Base Customer Count (derived\n from the Star Rating To Customer Count Curve) to determine the base\n number of customers per hour during off-lot simulation.\n ', tunable_type=float, minimum=0, default=0.5, tuning_group=GroupNames.OFF_LOT), 'off_lot_customer_count_penalty_multiplier': TunableRange( description= '\n A penalty multiplier applied to the off-lot customer count. This is\n applied after the Off Lot Customer Count Multiplier is applied.\n ', tunable_type=float, default=0.2, minimum=0, tuning_group=GroupNames.OFF_LOT), 'off_lot_chance_of_star_rating_increase': TunableRange( description= "\n Every time we run offlot simulations, we'll use this as the chance\n to increase in star rating instead of decrease.\n ", tunable_type=float, default=0.1, minimum=0, tuning_group=GroupNames.OFF_LOT), 'off_lot_star_rating_decay_per_hour_curve': TunableCurve( description= '\n Maps the current star rating of the business to the decay per hour\n of star rating value. This value will be added to the current star\n rating value so use negative numbers to make the rating decay.\n ', x_axis_name='Business Star Rating', y_axis_name='Off-Lot Star Rating Value Decay Per Hour', tuning_group=GroupNames.OFF_LOT), 'off_lot_star_rating_increase_per_hour_curve': TunableCurve( description= '\n Maps the current star rating of the business to the increase per\n hour of the star rating value, assuming the Off Lot Chance Of Star\n Rating Increase passes.\n ', x_axis_name='Business Star Rating', y_axis_name='Off-Lot Star Rating Value Increase Per Hour', tuning_group=GroupNames.OFF_LOT), 'off_lot_profit_per_item_multiplier': TunableRange( description= '\n This is multiplied by the average cost of the business specific\n service that is the main source of profit, to determine how much \n money the business makes per customer during off-lot simulation.\n ', tunable_type=float, default=0.3, minimum=0, tuning_group=GroupNames.OFF_LOT), 'off_lot_net_loss_notification': OptionalTunable( description= '\n If enabled, the notification that will show if a business turns a \n negative net profit during off-lot simulation.\n ', tunable=TunableUiDialogNotificationSnippet(), tuning_group=GroupNames.OFF_LOT), 'critic': OptionalTunable( description= '\n If enabled, allows tuning a critic for this business type.\n ', tunable=TunableTuple( description= '\n Critic tuning for this business.\n ', critic_trait=TunableReference( description= '\n The trait used to identify a critic of this business.\n ', manager=services.get_instance_manager( sims4.resources.Types.TRAIT)), critic_star_rating_application_count=TunableRange( description= '\n The number of times a critics star rating should count towards the\n business star rating.\n ', tunable_type=int, default=10, minimum=1), critic_star_rating_vfx_mapping=TunableStarRatingVfxMapping( description= '\n Maps the star rating for the critic to the persistent star effect\n that shows over their head.\n ' ), critic_banner_vfx=PlayEffect.TunableFactory( description= '\n A persistent banner VFX that is started when the critic\n arrives and stopped when they leave.\n ' )), tuning_group=GroupNames.CUSTOMER) } @classmethod def _verify_tuning_callback(cls): advertising_data_types = frozenset( cls.advertising_configuration.advertising_data_map.keys()) advertising_types_with_mapped_names = frozenset( cls.advertising_name_map.keys()) advertising_sort_ordered_types = frozenset( cls.advertising_name_map.keys()) if advertising_data_types: if advertising_data_types != advertising_types_with_mapped_names: logger.error( 'Advertising type list {} does not match list of mapped names: {}', advertising_data_types, advertising_types_with_mapped_names) if advertising_data_types != advertising_sort_ordered_types: logger.error( 'Advertising type list {} does not sorted UI list types: {}', advertising_data_types, advertising_sort_ordered_types) if cls.advertising_configuration.default_advertising_type is not None and cls.advertising_configuration.default_advertising_type not in advertising_types_with_mapped_names: logger.error( 'Default advertising type {} is not in advertising name map', cls.default_advertising_type)
class RankedStatistic( HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC) ): @classmethod def _verify_tuning_callback(cls): super()._verify_tuning_callback() ranks_tuned = [ level_data for level_data in cls.event_data.values() if level_data.rank_up ] ranks_needed = len(ranks_tuned) + 1 actual_ranks = len(cls.rank_tuning) tuned_rank_up_notifications = len(cls.rank_up_notification_tuning) tuned_rank_down_notifications = len(cls.rank_down_notification_tuning) if actual_ranks != ranks_needed: logger.error( '{} ranks have been enabled, but there is tuning for {} ranks in the rank_tuning. Please double check the tuning for {}', ranks_needed, actual_ranks, cls) if actual_ranks != tuned_rank_up_notifications: logger.error( 'There are {} ranks tuned but {} rank up notifications tuned. These need to be the same. Please double check the tuning for {}', actual_ranks, tuned_rank_up_notifications, cls) if tuned_rank_down_notifications > 0 and actual_ranks != tuned_rank_down_notifications: logger.error( 'There are {} ranks tuned but {} rank down notifications tuned. These need to be the same. Please double check the tuning for {}', actual_ranks, tuned_rank_down_notifications, cls) INSTANCE_TUNABLES = { 'stat_name': TunableLocalizedString( description= '\n Localized name of this statistic.\n ', allow_none=True), 'event_intervals': TunableList( description= '\n The level boundaries for an event, specified as a delta from the\n previous value.\n ', tunable=Tunable( description= '\n Points required to reach this level.\n ', tunable_type=int, default=0), export_modes=ExportModes.All), 'event_data': TunableMapping( description= '\n The data associated with a specific tuned event. \n \n The Key is the event number as tuned in the event intervals.\n \n The value is a list of loots to apply when the event occurs and an\n bool for whether or not to rank up the stat. \n ', key_type=int, value_type=TunableTuple( description= '\n The data associated with a tuned event from event_intervals.\n ', rank_up=Tunable( description= "\n If checked then this event will cause the statistic to rank\n up and all that entails. Currently that will increment\n the rank count.\n \n There should be a rank up entry for each of the levels \n tuned, except the initial rank. We assume that you don't \n need to rank into the initial rank. This means you will \n need one more level tuned than number of rank up events\n found in this list.\n ", tunable_type=bool, default=False), loot=TunableList( description= '\n A list of loots to apply when this event happens. This loot\n is only applied the first time you reach a specific level.\n If you want the loot applied every time you reach a level\n (for instance after you decay to a previous level and then\n regain a level) please use the loot_always tuning.\n ', tunable=TunableReference( description= '\n The loot to apply.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), tooltip=TunableLocalizedStringFactory( description= '\n The tooltip to display in the UI for each of the event\n lines. This is to be used for telling the user what loot \n they are going to get at an individual event.\n ' ), level_down_loot=TunableList( description= '\n A list of loots to apply when the Sim loses enough points \n to level down.\n ', tunable=LootActions.TunableReference(pack_safe=True)), tests=event_testing.tests.TunableTestSet( description= "\n Tests to run when reaching this level. If the tests don't \n pass then the value will be set back to min points for \n the rank before it. This means that the Sim won't be able\n to make any progress towards the rank with the failed\n tests.\n ", export_modes=ExportModes.ServerXML), loot_always=TunableList( description= '\n This loot is always awarded on level up, regardless of \n whether or not this level has already been achieved or not.\n \n If you want the loot to only be applied the first time you\n reach a certain level then please use the loot tuning.\n ', tunable=TunableReference( description= '\n The loot to award on level up.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), loot_always_on_load=TunableList( description= '\n This loot is always awarded when a sim loads with this\n level.\n ', tunable=LootActions.TunableReference(pack_safe=True)), export_class_name='EventDataTuple'), tuple_name='TunableEventData', export_modes=ExportModes.All), 'initial_rank': Tunable(description= '\n The initial rank for this stat.\n ', tunable_type=int, default=1, export_modes=ExportModes.All), 'rank_tuning': TunableMapping( description= '\n This is the tuning that is associated with a specific rank level \n instead of each individual event level. When the rank has increased \n the matching information will be retrieved from here and used.\n \n There needs to be an equal number of ranks tuned to match all of \n the rank up events in event data plus an extra one for the \n rank you start out on initially.\n ', key_type=int, value_type=TunableTuple( description= '\n A tuple of all the data for each Rank associated wit this\n ranked statistic.\n ', rank_name=TunableLocalizedString( description= "\n The rank's normal name.\n " ), icon=OptionalTunable( description= '\n If enabled then the Rank Statistic will have an icon \n associated with this Rank.\n ', tunable=TunableResourceKey( description= '\n Icon to be displayed for the rank.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), enabled_by_default=True), rank_description=OptionalTunable( description= '\n When enabled this string will be used as the description\n for the rank.\n ', tunable=TunableLocalizedString( description= "\n The rank's description.\n " )), rank_short_name=OptionalTunable( description= '\n When enabled this string will be used as an alternate \n short name for the rank.\n ', tunable=TunableLocalizedString( description= "\n The rank's short name.\n " )), hide_in_ui=Tunable( description= '\n If checked, this rank will not be shown in some places in the UI (XP bars, Relationship tooltip, Gallery)\n ', tunable_type=bool, default=False), export_class_name='RankDataTuple'), tuple_name='TunableRankData', export_modes=ExportModes.All), 'rank_down_notification_tuning': TunableMapping( description= '\n A mapping of Rank to tuning needed to display all the notifications\n when a Sim ranks down. \n \n The number of notifications tuned must match the number of items\n in rank_tuning.\n ', key_type=int, value_type=TunableTuple( description= '\n A Tuple containing both the rank down screen slam and the rank\n down notification to display.\n ', show_notification_tests=event_testing.tests.TunableTestSet( description= '\n Tests that must be true when the we want to show notification.\n ' ), rank_down_screen_slam=OptionalTunable( description= '\n Screen slam to show when Sim goes down to this rank level.\n Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n {1.String}, Rank Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), rank_down_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this\n rank. The text will be provided two tokens: the Sim owning\n the stat and a number representing the 1-based rank\n level.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })))), 'rank_up_notification_tuning': TunableMapping( description= '\n A mapping of Rank to tuning needed to display all the notifications\n when a Sim ranks up. \n \n The number of notifications tuned must match the number of items\n in rank_tuning.\n ', key_type=int, value_type=TunableTuple( description= '\n A Tuple containing both the rank up screen slam and the rank\n up notification to display.\n ', show_notification_tests=event_testing.tests.TunableTestSet( description= '\n Tests that must be true when the we want to show notification.\n ' ), rank_up_screen_slam=OptionalTunable( description= '\n Screen slam to show when reaches this rank level.\n Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n {1.String}, Rank Number - {2.Number}\n \n This will only happen the first time a rank is reached.\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), rank_up_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this\n rank. The text will be provided two tokens: the Sim owning\n the stat and a number representing the 1-based rank\n level.\n \n This will only happen the first time a rank is reached. If\n you want to show a display on subsequent rank ups you can \n tune the re_rank_up_notifcation.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })), re_rank_up_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this rank\n every time other than the first time. For instance if the\n Sim achieves rank 3, drops down to rank 2 because of decay,\n and then re-achieves rank 3, that is when this dialog will\n be displayed.\n \n If you want this dialog to be displayed the first time the\n Sim reaches a rank please tune rank_up_notification instead.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })))), 'tags': TunableList( description= '\n The associated categories of the ranked statistic.\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True)), 'icon': TunableIcon( description="\n The ranked stat's icon.\n ", allow_none=True), 'initial_loot': TunableList( description= '\n A list of loots to apply when the Sim first receives this ranked\n statistic.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'min_decay_per_highest_level_achieved': TunableMapping( description= '\n A mapping of highest level reached to the absolute minimum \n that this Ranked Stat is allowed to decay to in ranks.\n ', key_type=int, value_type=TunableRange( description= '\n The lowest level this stat can decay to based on the associated\n highest level reached.\n ', tunable_type=int, minimum=1, default=1)), 'associated_bucks_types': TunableList( description= '\n A list of bucks types that are associated with this ranked stat.\n These bucks types may have tuned data that is affected by ranking\n up/down.\n ', tunable=TunableEnumEntry( description= '\n A buck type that is associated with this ranked stat.\n ', tunable_type=BucksType, default=BucksType.INVALID), unique_entries=True, export_modes=ExportModes.All), 'zero_out_on_lock': Tunable( description= '\n If checked, when this ranked stat is locked it will zero out\n the value, highest_level, and bucks.\n ', tunable_type=bool, default=True), 'headline': OptionalTunable( description= '\n If enabled when this relationship track updates we will display\n a headline update to the UI.\n ', tunable=TunableReference( description= '\n The headline that we want to send down.\n ', manager=services.get_instance_manager( sims4.resources.Types.HEADLINE)), tuning_group=GroupNames.UI), 'send_stat_update_for_npcs': Tunable( description= "\n If checked then whenever we attempt to send the ranked stat update\n message it will be sent, even if the Sim is an NPC.\n \n NOTE: We don't want to mark very many of the stats like this. This \n is being done to make sure that Fame gets sent so we don't have\n to request Fame when building the tooltip for sims which could be\n really slow.\n ", tunable_type=bool, default=False), 'center_bar_tooltip': Tunable( description= '\n If true, always put motive panel ranked stat bar tooltip at the center.\n If false, put tooltip on each increment mark instead.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), 'visible': Tunable( description= '\n Whether or not statistic should be sent to client.\n \n NOTE: Please work with your UI engineering partner to determine if this \n should be True. If False, for performance reasons, \n the stat will be removed from the sim if their\n current value matches the default convergence value. \n ', tunable_type=bool, default=False, export_modes=ExportModes.All) } REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning') def __init__(self, tracker): self._rank_level = self.initial_rank self.highest_level = 0 super().__init__(tracker, self.initial_value) self._current_event_level = 0 self.previous_event_level = 0 self._notifications_disabled = False self._initial_loots_awarded = False self._suppress_level_up_telemetry = False @constproperty def is_ranked(): return True @property def rank_level(self): return self._rank_level @property def process_non_selectable_sim(self): return True @rank_level.setter def rank_level(self, value): self._rank_level = value services.get_event_manager().process_event( TestEvent.RankedStatisticChange, sim_info=self.tracker.owner.sim_info) @property def highest_rank_achieved(self): rank_level = self.initial_rank for i in range(1, self.highest_level + 1): if self.event_data.get(i).rank_up: rank_level += 1 return rank_level @property def is_visible(self): if self.tracker is None or not self.tracker.owner.is_sim: return False return self.visible @property def instance_required(self): if self.is_visible: return True return super().instance_required def increase_rank_level(self, new_rank=True, from_add=False): self.rank_level += 1 self._on_rank_up(new_rank=new_rank, from_add=from_add) def increase_rank_levels(self, levels): start_level = self.rank_level self.rank_level = start_level + levels self.send_rank_change_update_message(start_level, start_level + levels) def decrease_rank_level(self): self.rank_level = max(self.rank_level - 1, 0) self._on_rank_down() def _on_rank_up(self, new_rank=True, from_add=False): current_rank = self.rank_level self.send_rank_change_update_message(current_rank - 1, current_rank) sim_info = self.tracker.owner.sim_info rank_data = self.rank_tuning.get(current_rank) rank_up_data = self.rank_up_notification_tuning.get(current_rank) if rank_data is None: logger.error( 'Sim {}: {} is trying to rank up to level {} but there is no rank tuning.', sim_info, self, current_rank) return if not from_add and (sim_info.is_selectable and rank_up_data is not None ) and self.can_show_notification(rank_up_data): icon_override = None if rank_data.icon is None else IconInfoData( icon_resource=rank_data.icon) if new_rank: self._show_initial_rank_up_notifications( sim_info, current_rank, rank_data, rank_up_data, icon_override) else: self._show_re_rank_up_notifications(sim_info, current_rank, rank_data, rank_up_data, icon_override) def _show_initial_rank_up_notifications(self, sim_info, current_rank, rank_data, rank_up_data, icon_override): if rank_up_data.rank_up_notification is not None: notification = rank_up_data.rank_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog( icon_override=icon_override, secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(current_rank, )) if rank_up_data.rank_up_screen_slam is not None: rank_up_data.rank_up_screen_slam.send_screen_slam_message( sim_info, sim_info, rank_data.rank_name, current_rank) def _show_re_rank_up_notifications(self, sim_info, current_rank, rank_data, rank_up_data, icon_override): if rank_up_data.re_rank_up_notification is not None: notification = rank_up_data.re_rank_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog( icon_override=icon_override, secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(current_rank, )) def _on_rank_down(self): current_rank = self.rank_level self.send_rank_change_update_message(current_rank + 1, current_rank) sim_info = self.tracker.owner.sim_info rank_data = self.rank_tuning.get(current_rank) rank_down_data = self.rank_down_notification_tuning.get(current_rank) if rank_data is None: logger.error( 'Sim {}: {} is trying to rank down to level {} but there is no rank tuning.', sim_info, self, current_rank) return if self.can_show_notification(rank_down_data): if rank_down_data.rank_down_notification is not None: notification = rank_down_data.rank_down_notification( sim_info, resolver=SingleSimResolver(sim_info)) icon_override = None if rank_data.icon is None else IconInfoData( icon_resource=rank_data.icon) notification.show_dialog(icon_override=icon_override, secondary_icon_override=IconInfoData( obj_instance=sim_info), additional_tokens=(current_rank, )) if rank_down_data.rank_down_screen_slam is not None: rank_down_data.rank_down_screen_slam.send_screen_slam_message( sim_info, sim_info, rank_data.rank_name, current_rank) for bucks_type in self.associated_bucks_types: bucks_tracker = BucksUtils.get_tracker_for_bucks_type( bucks_type, owner_id=self.tracker.owner.id) bucks_tracker.validate_perks(bucks_type, self.rank_level) def on_add(self): super().on_add() self.tracker.owner.sim_info.on_add_ranked_statistic() self.on_stat_event(self.highest_level, self.get_user_value(), from_add=True) self.previous_event_level = self.get_user_value() if self.tracker.owner.is_simulating: self.apply_initial_loot() @classmethod def get_level_list(cls): return list(cls.event_intervals) def on_initial_startup(self): super().on_initial_startup() self.decay_enabled = self.tracker.owner.is_selectable and not self.tracker.owner.is_locked( self) @staticmethod def _callback_handler(stat_inst): new_level = stat_inst.get_user_value() stat_inst.on_stat_event(stat_inst.previous_event_level, new_level) stat_inst.previous_event_level = new_level stat_inst.refresh_threshold_callback() def on_stat_event(self, old_level, new_level, from_add=False): batch_rank_levels = 0 while old_level < new_level: old_level += 1 event_data = self.event_data.get(old_level) if event_data is not None: if self.tracker.owner.is_simulating: resolver = SingleSimResolver(self.tracker.owner) is_new_level = old_level > self.highest_level if is_new_level: for loot in event_data.loot: loot.apply_to_resolver(resolver) self.highest_level = old_level if event_data.rank_up: self.increase_rank_level(new_rank=is_new_level, from_add=from_add) for loot in event_data.loot_always: loot.apply_to_resolver(resolver) elif event_data.rank_up: batch_rank_levels += 1 if self.tracker.owner.is_npc: if not from_add: self._handle_level_up_telemetry(old_level) self._handle_level_up_telemetry(old_level) if batch_rank_levels > 0: self.increase_rank_levels(batch_rank_levels) else: self.create_and_send_commodity_update_msg(is_rate_change=False) @contextlib.contextmanager def suppress_level_up_telemetry(self): if self._suppress_level_up_telemetry: yield None else: self._suppress_level_up_telemetry = True try: yield None finally: self._suppress_level_up_telemetry = False def _handle_level_up_telemetry(self, level): if not self._suppress_level_up_telemetry: with telemetry_helper.begin_hook( ranked_stat_telemetry_writer, TELEMETRY_HOOK_RANKED_STAT_LEVEL_UP) as hook: hook.write_guid(TELEMETRY_FIELD_RANKED_STAT_TYPE, self.guid64) hook.write_int(TELEMETRY_FIELD_RANKED_STAT_LEVEL, level) @sims4.utils.classproperty def max_value(cls): return cls.get_max_skill_value() @sims4.utils.classproperty def min_value(cls): return 0 @sims4.utils.classproperty def best_value(cls): return cls.max_value @sims4.utils.classproperty def max_rank(cls): (_, rank) = cls.calculate_level_and_rank(cls.max_value) return rank @classmethod def convert_to_user_value(cls, value): if not cls.get_level_list(): return 0 current_value = value for (level, level_threshold) in enumerate(cls.get_level_list()): current_value -= level_threshold if current_value < 0: return level return level + 1 def can_show_notification(self, rank_data): if self._notifications_disabled: return False elif rank_data is not None and rank_data.show_notification_tests is not None: resolver = event_testing.resolver.SingleSimResolver( self.tracker.owner) result = rank_data.show_notification_tests.run_tests(resolver) if not result: return False return True def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_points = self.get_value() old_user_value = self.get_user_value() value_to_set = value if not from_load: value_to_set = self._get_valid_value(value, old_user_value) minimum_level = self._get_minimum_decay_level() value_to_set = max(value_to_set, minimum_level) super().set_value(value_to_set, *args, from_load=from_load, interaction=interaction, **kwargs) new_user_value = self.get_user_value() if not from_load: self._handle_level_down(old_user_value, new_user_value) sim_info = self._tracker._owner new_points = self.get_value() stat_type = self.stat_type if old_points == self.initial_value or old_points != new_points: services.get_event_manager().process_event( TestEvent.StatValueUpdate, sim_info=sim_info, statistic=stat_type, custom_keys=(stat_type, )) self.send_commodity_progress_msg(is_rate_change=False) self.send_change_update_message(value - old_points, from_load=from_load) self.previous_event_level = new_user_value self.refresh_threshold_callback() def _update_value(self): minimum_decay = self._get_minimum_decay_level() old_value = self._value old_user_value = self.convert_to_user_value(self._value) super()._update_value(minimum_decay_value=minimum_decay) new_value = self._value new_user_value = self.convert_to_user_value(self._value) self._handle_level_down(old_user_value, new_user_value) if old_user_value > new_user_value: self.previous_event_level = new_user_value self.refresh_threshold_callback() stat_type = self.stat_type if new_value > old_value: sim_info = self._tracker._owner if self._tracker is not None else None services.get_event_manager().process_event( TestEvent.StatValueUpdate, sim_info=sim_info, statistic=stat_type, custom_keys=(stat_type, )) def _get_minimum_decay_level(self): min_rank = self.min_decay_per_highest_level_achieved.get( self.highest_level, None) if min_rank is None: return 0 points = self.points_to_rank(min_rank) return points def _handle_level_down(self, old_value, new_value): while new_value < old_value: event_data = self.event_data.get(old_value) if event_data is not None: resolver = SingleSimResolver(self.tracker.owner) for loot in event_data.level_down_loot: loot.apply_to_resolver(resolver) if event_data.rank_up: self.decrease_rank_level() old_value -= 1 def get_next_rank_level(self): current_value = self.get_user_value() index = current_value + 1 if index > len(self.event_data): return current_value while not self.event_data[index].rank_up: if index == len(self.event_data): break index += 1 return index @constproperty def remove_on_convergence(): return False def send_commodity_progress_msg(self, is_rate_change=True): self.create_and_send_commodity_update_msg( is_rate_change=is_rate_change) @classmethod def points_to_level(cls, event_level): level = 0 running_sum = 0 level_list = cls.get_level_list() while level < len(level_list): while level < event_level: running_sum += level_list[level] level += 1 return running_sum @classmethod def points_to_rank(cls, rank_level): rank = cls.initial_rank level = 0 running_sum = 0 level_list = cls.get_level_list() while rank < rank_level: while level < len(level_list): event_data = cls.event_data.get(level) if event_data is not None: if cls.event_data[level].rank_up: rank += 1 if rank < rank_level: running_sum += level_list[level] level += 1 return running_sum def points_to_current_rank(self): return self.points_to_rank(self.rank_level) def create_and_send_commodity_update_msg(self, is_rate_change=True, allow_npc=False, from_add=False): ranked_stat_msg = Commodities_pb2.RankedStatisticProgressUpdate() ranked_stat_msg.stat_id = self.guid64 ranked_stat_msg.change_rate = self.get_change_rate() ranked_stat_msg.rank = self.rank_level difference = self.get_value() - self.points_to_current_rank() ranked_stat_msg.curr_rank_points = int( difference) if difference > 0 else 0 send_sim_ranked_stat_update_message(self.tracker.owner, ranked_stat_msg, allow_npc=allow_npc or self.send_stat_update_for_npcs) @classmethod def send_commodity_update_message(cls, sim_info, old_value, new_value): commodity_tracker = sim_info.commodity_tracker if commodity_tracker is None: return stat_instance = commodity_tracker.get_statistic(cls) if stat_instance is None: return stat_instance.create_and_send_commodity_update_msg(is_rate_change=True) def send_change_update_message(self, amount, from_load=False): if from_load: return if self.headline is None: return sim = self.tracker.owner if sim.is_selectable: self.headline.send_headline_message(sim, amount) def send_rank_change_update_message(self, previous_rank, current_rank): msg = Commodities_pb2.RankedStatisticRankChangedUpdate() msg.stat_id = self.guid64 msg.previous_rank = previous_rank msg.current_rank = current_rank send_sim_ranked_stat_change_rank_change_update_message( self.tracker.owner, msg) self.send_commodity_progress_msg() def on_sim_ready_to_simulate(self): level = self.get_user_value() event_data = self.event_data.get(level) if event_data is not None: resolver = SingleSimResolver(self.tracker.owner) for loot in event_data.loot_always_on_load: loot.apply_to_resolver(resolver) self.apply_initial_loot() def apply_initial_loot(self): if not self.initial_loot: return if self._initial_loots_awarded: return resolver = SingleSimResolver(self.tracker.owner) for loot in self.initial_loot: loot.apply_to_resolver(resolver) self._initial_loots_awarded = True def _get_valid_value(self, value, old_score): new_score = self.convert_to_user_value(value) if old_score <= new_score: resolver = SingleSimResolver(self.tracker.owner) while old_score <= new_score: old_score += 1 event_data = self.event_data.get(old_score) if event_data is not None: if not event_data.tests.run_tests(resolver=resolver): points = self.points_to_level(old_score - 1) return points return value def on_lock(self, action_on_lock): self._notifications_disabled = True should_zero_out = self.zero_out_on_lock or action_on_lock == StatisticLockAction.USE_MIN_VALUE_TUNING if should_zero_out: self.highest_level = 0 super().on_lock(action_on_lock) if should_zero_out: self.reset_bucks() self._notifications_disabled = False def reset_bucks(self): for bucks_type in self.associated_bucks_types: bucks_tracker = BucksUtils.get_tracker_for_bucks_type( bucks_type, self.tracker.owner.id) if bucks_tracker is not None: bucks_tracker.try_modify_bucks( bucks_type, -bucks_tracker.get_bucks_amount_for_type(bucks_type)) @classmethod def calculate_level_and_rank(cls, value): level = 0 rank = cls.initial_rank for points_to_next_level in cls.get_level_list(): value -= points_to_next_level if value < 0: break level += 1 level_data = cls.event_data.get(level) if level_data.rank_up: rank += 1 return (level, rank) def set_level_and_rank(self): (level, rank) = self.calculate_level_and_rank(self.get_value()) self.previous_event_level = level self.rank_level = rank def should_display_delayed_decay_warning(self): if self.highest_level == 0: return False return super().should_display_delayed_decay_warning() @classproperty def valid_for_stat_testing(cls): return True @classmethod def load_statistic_data(cls, tracker, data): super().load_statistic_data(tracker, data) stat = tracker.get_statistic(cls) if stat is not None: stat._initial_loots_awarded = data.initial_loots_awarded stat.set_level_and_rank() stat.highest_level = data.highest_level stat.load_time_of_last_value_change(data) stat.fixup_callbacks_during_load() def save_statistic(self, commodities, skills, ranked_statistics, tracker): message = protocols.RankedStatistic() message.name_hash = self.guid64 message.value = self.get_saved_value() message.highest_level = self.highest_level message.initial_loots_awarded = self._initial_loots_awarded if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks( ) ranked_statistics.append(message)
class Season(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.SEASON)): INSTANCE_TUNABLES = { 'season_icon': TunableIcon( description="\n The season's icon.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'season_name': TunableLocalizedString( description="\n The season's name.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'season_length_content': TunableMapping( description= '\n A mapping of season length option to the content contained within.\n ', key_type=TunableEnumEntry(tunable_type=SeasonLength, default=SeasonLength.NORMAL), value_type=SeasonalContent.TunableFactory()), 'screen_slam': OptionalTunable( description= '\n If enabled, trigger this Screen Slam when transitioning to this season.\n ', tunable=TunableTuple( description= '\n The screenslam to trigger, and hour of the day when it should\n appear to the users.\n ', slam=TunableScreenSlamSnippet(), trigger_time=TunableTimeOfDay(default_hour=6))), 'whim_set': OptionalTunable( description= '\n If enabled then this season will offer a whim set to the Sim\n when it is that season.\n ', tunable=TunableReference( description= '\n A whim set that is active when this season is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.ASPIRATION), class_restrictions=('ObjectivelessWhimSet', ))) } def __init__(self, start_time, **kwargs): super().__init__(**kwargs) self._start_time = start_time self._length_option = None self._length_span = None self._content_data = None self._mid_season_begin = None self._absolute_mid = None self._late_season_begin = None self._end_of_season = None def __contains__(self, date_and_time): return self._start_time <= date_and_time < self._end_of_season @property def info(self): holiday_formatted = '\n\t'.join([ '{} on {}'.format(holiday.__name__, day_of_season.date_and_time) for (holiday, day_of_season) in self.get_holiday_dates() ]) return 'Resource: {}\nLength: {}\nStart: {}\n\tMid-Season Period: {}\n\tAbsolute Mid-Season: {}\n\tLate-Season Period: {}\nEnd: {}\nHolidays:\n\t{}'.format( self.__class__, self._length_span, self._start_time, self._mid_season_begin, self._absolute_mid, self._late_season_begin, self._end_of_season, holiday_formatted) @property def start_time(self): return self._start_time @property def length(self): return self._length_span @property def end_time(self): return self._end_of_season @property def midpoint_time(self): return self._absolute_mid def get_date_at_season_progress(self, progress): progress = clamp(0, progress, 1) return self._start_time + self._length_span * progress def get_position(self, date_and_time): return date_and_time - self._start_time def get_segment(self, date_and_time): if not self._verify_in_season(date_and_time): return if date_and_time < self._mid_season_begin: return SeasonSegment.EARLY if date_and_time >= self._late_season_begin: return SeasonSegment.LATE return SeasonSegment.MID def get_progress(self, date_and_time): if not self._verify_in_season(date_and_time): return current_ticks = self.get_position(date_and_time).in_ticks() total_ticks = self._length_span.in_ticks() return current_ticks / total_ticks def get_screen_slam_trigger_time(self): if self.screen_slam is None: return return self._start_time.time_of_next_day_time( self.screen_slam.trigger_time) def _verify_in_season(self, date_and_time): within_season = date_and_time in self if not within_season: seasons_logger.error( 'Provided time {} is not within the current season, which is from {} to {}', date_and_time, self._start_time, self._end_of_season) return within_season def set_length_option(self, length_option): if self._length_option == length_option: return self._length_option = length_option self._length_span = SeasonsTuning.SEASON_LENGTH_OPTIONS[length_option]( ) self._calculate_important_dates() def _calculate_important_dates(self): self._content_data = self.season_length_content[self._length_option] self._mid_season_begin = self._start_time + self._content_data.segments.early_season_length( ) self._absolute_mid = self.get_date_at_season_progress(0.5) self._late_season_begin = self._start_time + ( self._length_span - self._content_data.segments.late_season_length()) self._end_of_season = self._start_time + self._length_span def get_holiday_dates(self): holidays_in_season = [] for (holiday, season_times) in self._content_data.holidays.items(): holidays_in_season.extend( iter((holiday, day_of_season(self._start_time)) for day_of_season in season_times)) return holidays_in_season def get_all_holiday_data(self): holidays_data = [] for season_length in SeasonLength: for ( holiday, season_times ) in self.season_length_content[season_length].holidays.items(): holidays_data.extend( iter((season_length, holiday, day(date_and_time.DATE_AND_TIME_ZERO).day_of_season) for day in season_times)) return holidays_data def get_holidays(self, season_length): return set(self._content_data.holidays.keys())
class ObjectAnimationElement(HasTunableReference, elements.ParentElement, metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.ANIMATION)): ASM_SOURCE = 'asm_key' INSTANCE_TUNABLES = {ASM_SOURCE: TunableInteractionAsmResourceKey(description='\n The ASM to use.\n ', default=None, category='asm'), 'actor_name': Tunable(description='\n The name of the actor in the ASM.\n ', tunable_type=str, default=None, source_location=ASM_SOURCE, source_query=SourceQueries.ASMActorAll), 'target_name': OptionalTunable(description='\n If enabled, some portion of this object animation expects the actor\n to interact with another object. The object must be set by whatever\n system uses the ASM. In and of itself, the Idle component never sets\n this actor.\n ', tunable=Tunable(tunable_type=str, default=None, source_location='../' + ASM_SOURCE, source_query=SourceQueries.ASMActorAll)), 'initial_state': OptionalTunable(description='\n The name of the initial state in the ASM you expect your actor to be\n in when running this AnimationElement. If you do not tune this we\n will use the entry state which is usually what you want.\n ', tunable=Tunable(tunable_type=str, default=None, source_location='../' + ASM_SOURCE, source_query=SourceQueries.ASMState), disabled_value=DEFAULT, disabled_name='use_default', enabled_name='custom_state_name'), 'begin_states': TunableList(description='\n A list of states to play.\n ', tunable=str, source_location=ASM_SOURCE, source_query=SourceQueries.ASMState), 'end_states': TunableList(description='\n A list of states to play after looping.\n ', tunable=str, source_location=ASM_SOURCE, source_query=SourceQueries.ASMState), 'balloon_data': OptionalTunable(description='\n Information that will be used to create an element that represents\n thought or speech balloons.\n ', tunable=TunableTuple(icon=TunableIcon(description='\n Icon that will be used in the balloon. Object Animations\n elements only support Icon References.\n '), balloon_type=TunableEnumEntry(description='\n The visual style of the balloon background.\n ', tunable_type=BalloonTypeEnum, default=BalloonTypeEnum.SPEECH), overlay=OptionalTunable(description='\n The overlay of the balloon, if present.\n ', tunable=TunableIcon()), balloon_delay=Tunable(description='\n If set, the number of seconds after the start of the animation\n to trigger the balloon. A negative number will count backwards\n from the end of the animation.\n ', tunable_type=int, default=0), balloon_delay_random_offset=Tunable(description='\n The amount of randomization that added to balloon requests.\n Will always offset the delay time later and requires the delay\n time be set to a number. A value of 0 has no randomization.\n ', tunable_type=int, default=0))), 'repeat': Tunable(description='\n If this is checked, then the begin_states will loop until the\n controlling sequence (e.g. state change on idle component) ends. \n At that point, end_states will play.\n \n This tunable allows you to create looping one-shot states. The\n effects of this tunable on already looping states is undefined.\n ', tunable_type=bool, default=False), 'use_surface_height': Tunable(description='\n If checked, the asm will be provided with the surfaceHeight\n global parameter, which uses slot height tuning to resolve the \n height of the target object to a parameter value. \n ', tunable_type=bool, default=False)} def __init__(self, owner, use_asm_cache=True, target=None, use_surface_height=False, **animate_kwargs): super().__init__() self.owner = owner self.target = target self.animate_kwargs = animate_kwargs self._use_asm_cache = use_asm_cache self._use_surface_height = use_surface_height @classmethod def append_to_arb(cls, asm, arb): for state_name in cls.begin_states: asm.request(state_name, arb) @classmethod def append_exit_to_arb(cls, asm, arb): for state_name in cls.end_states: asm.request(state_name, arb) def get_asm(self, use_cache=True, **kwargs): idle_component = self.owner.get_component(IDLE_COMPONENT) if idle_component is None: logger.error('Trying to setup an object animation {}, {} on an object {} with no Idle Component.', self, self.asm_key, self.owner) return asm = idle_component.get_asm(self.asm_key, self.actor_name, use_cache=self._use_asm_cache and use_cache, **kwargs) if self.target is not None: if self.target_name is not None: asm.add_potentially_virtual_actor(self.actor_name, self.owner, self.target_name, self.target) if self.use_surface_height: surface_height = get_surface_height_parameter_for_object(self.target) asm.set_parameter('surfaceHeight', surface_height) return asm def _run(self, timeline): if self.asm_key is None: return True asm = self.get_asm() if asm is None: return False if self.balloon_data is not None: sequence = self.animate_kwargs['sequence'] if sequence is not None: self.animate_kwargs['sequence'] = (sequence, build_element(self.create_balloon_request(self.owner))) else: self.animation_kwargs['sequence'] = build_element(self.create_balloon_request(self.owner)) return timeline.run_child(animate_states(asm, self.begin_states, self.end_states, repeat_begin_states=self.repeat, **self.animate_kwargs)) def create_balloon_request(self, owner): (balloon_type, priority) = BALLOON_TYPE_LOOKUP[self.balloon_data.balloon_type] request = BalloonRequest(owner, self.balloon_data.icon, None, self.balloon_data.overlay, balloon_type, priority, TunableBalloon.BALLOON_DURATION, self.balloon_data.balloon_delay, self.balloon_data.balloon_delay_random_offset, None) return request
class PrepTask(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'statistic': TunableReference(description='\n Statistic that is tracked by this prep task.\n ', manager=services.get_instance_manager(Types.STATISTIC)), 'linked_statistics': TunableList(description='\n If specified, these are statistics whose value updates\n are linked to the specified statistic.\n \n Value changes to the linked statistic are applied with\n the tuned multiplier to the statistic.\n ', tunable=TunableTuple(stat_type=TunableReference(manager=services.get_instance_manager(Types.STATISTIC)), multiplier=TunableRange(tunable_type=float, minimum=0.0, default=1.0))), 'task_icon': TunableIcon(description='\n The icon to use in displaying the prep task.\n '), 'task_description': TunableLocalizedStringFactory(description='\n A description of the prep task. {0.String}\n is the thresholded description.\n '), 'task_tooltip': OptionalTunable(description='\n If enabled, tooltip will show up on the preptask.\n ', tunable=TunableLocalizedStringFactory(description='\n A tooltip of the prep task. \n ')), 'thresholded_descriptions': TunableList(description='\n A list of thresholds and the text describing it. The\n thresholded description will be largest threshold\n value in this list that the commodity is >= to.\n ', tunable=TunableTuple(threshold=Tunable(description='\n Threshold that the commodity must >= to.\n ', tunable_type=float, default=0.0), text=TunableLocalizedString(description='\n Description for meeting this threshold.\n ')))} def get_prep_task_progress_thresholds(self, sim_info): lower_threshold = None upper_threshold = None stat = sim_info.get_statistic(self.statistic) value = stat.get_value() for threshold in self.thresholded_descriptions: if value >= threshold.threshold: if lower_threshold is None or threshold.threshold > lower_threshold.threshold: lower_threshold = threshold if value < threshold.threshold: if not upper_threshold is None: if threshold.threshold < upper_threshold.threshold: upper_threshold = threshold upper_threshold = threshold return (lower_threshold, upper_threshold) def get_prep_task_display_name(self, sim_info): loc_strings = [] lower_threshold = None stat = sim_info.get_statistic(self.statistic) if stat is not None: (lower_threshold, _) = self.get_prep_task_progress_thresholds(sim_info) if lower_threshold: description = self.task_description(lower_threshold.text) else: description = self.task_description() loc_strings.append(description) if loc_strings: return LocalizationHelperTuning.get_new_line_separated_strings(*loc_strings) def is_completed(self, sim_info): stat = sim_info.get_statistic(self.statistic) if stat is None: return False return stat.get_value() >= stat.max_value
class WeatherForecast(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( Types.WEATHER_FORECAST)): INSTANCE_TUNABLES = { 'calendar_icon': TunableIcon( description= '\n The small icon for this forecast.\n ', export_modes=ExportModes.All), 'calendar_icon_large': TunableIcon( description= '\n The large icon for this forecast.\n ', export_modes=ExportModes.All), 'calendar_icon_mascot': TunableIcon( description= '\n Optional icon to use as the forecast mascot in the calendar.\n ', allow_none=True, export_modes=ExportModes.All), 'forecast_description': TunableLocalizedString( description= '\n The description for this forecast.\n ', export_modes=ExportModes.All), 'forecast_name': TunableLocalizedString( description= '\n The name for this forecast.\n ', export_modes=ExportModes.All), 'prescribed_weather_type': OptionalTunable( description= '\n The types of prescribed weather this forecast counts as\n ', tunable=TunableTuple( rain=Tunable( description= '\n If checked this forecast will be unavailable if rain is disabled\n ', tunable_type=bool, default=False), storm=Tunable( description= '\n If checked this forecast will be unavailable if storm is disabled\n ', tunable_type=bool, default=False), snow=Tunable( description= '\n If checked this forecast will be unavailable if snow is disabled\n ', tunable_type=bool, default=False), blizzard=Tunable( description= '\n If checked this forecast will be unavailable if blizzard is disabled\n ', tunable_type=bool, default=False))), 'weather_event_time_blocks': TunableMapping( description= '\n The weather events that make up this forecast. Key is hour of day\n that event would start, value is a list of potential events\n ', key_type=Tunable(tunable_type=int, default=0), value_type=TunableList( description= '\n List of the weather events that can occur in this time block\n ', tunable=TunableTuple( description= '\n A tuple of information for the weather event.\n ', weather_event=WeatherEvent.TunableReference( description= '\n The weather event.\n ', pack_safe=True), duration=TunableInterval( description= '\n Minimum and maximum time, in sim hours, this event can last.\n ', tunable_type=float, default_lower=1, default_upper=4), weight=Tunable( description= '\n Weight of this event being selected.\n ', tunable_type=int, default=1)))), 'weather_ui_override': TunableMapping( description= '\n If set, this overrides the weather type that is shown for the\n specified group.\n ', key_type=TunableEnumEntry( tunable_type=WeatherTypeGroup, default=WeatherTypeGroup.UNGROUPED, invalid_enums=(WeatherTypeGroup.UNGROUPED, )), value_type=TunableEnumEntry( tunable_type=WeatherType, default=WeatherType.UNDEFINED, invalid_enums=(WeatherType.UNDEFINED, )), tuning_group=GroupNames.SPECIAL_CASES) } @classmethod def get_weather_event(cls): weather_schedule = [] for (beginning_hour, event_list) in cls.weather_event_time_blocks.items(): weather_schedule.append((beginning_hour, event_list)) weather_schedule.sort(key=operator.itemgetter(0)) time_of_day = services.time_service().sim_now hour_of_day = time_of_day.hour() entry = weather_schedule[-1] weather_events = entry[1] for entry in weather_schedule: if entry[0] <= hour_of_day: weather_events = entry[1] else: break weighted_events = [(weather_event.weight, weather_event) for weather_event in weather_events] chosen_weather_event = random.weighted_random_item(weighted_events) return (chosen_weather_event.weather_event, chosen_weather_event.duration.random_float()) @classmethod def is_snowy(cls): if cls.prescribed_weather_type is None: return False return cls.prescribed_weather_type.snow or cls.prescribed_weather_type.blizzard @classmethod def is_rainy(cls): if cls.prescribed_weather_type is None: return False return cls.prescribed_weather_type.rain or cls.prescribed_weather_type.storm @classmethod def is_forecast_supported(cls, options, snow_safe, rain_safe): prescribed_weather_type = cls.prescribed_weather_type if prescribed_weather_type is None: return True if prescribed_weather_type.rain and ( not rain_safe or options[PrecipitationType.RAIN] == WeatherOption.WEATHER_DISABLED): return False if prescribed_weather_type.snow and ( not snow_safe or options[PrecipitationType.SNOW] == WeatherOption.WEATHER_DISABLED): return False if prescribed_weather_type.storm and (not rain_safe or options[PrecipitationType.RAIN] == WeatherOption.DISABLE_STORMS): return False elif prescribed_weather_type.blizzard and ( not snow_safe or options[PrecipitationType.SNOW] == WeatherOption.DISABLE_STORMS): return False return True