class BuffRemovalOp(BaseLootOperation): __qualname__ = 'BuffRemovalOp' FACTORY_TUNABLES = { 'description': '\n This loot will remove buffs from a Sim.\n ', 'remove_all_visible_buffs': Tunable( description= "\n If checked, all visible buffs on the Sim, excluding those specified in\n the 'buffs_to_ignore' list will be removed. If unchecked, buff removal\n will be handled by the 'buffs_to_remove' list.\n ", tunable_type=bool, default=False), 'buffs_to_remove': TunableList( description= "\n If 'remove_all_buffs' is not checked, this is the list of buffs that\n will be removed from the subject. If 'remove_all_buffs' is checked,\n this list will be ignored.\n ", tunable=TunableReference( description='\n Buff to be removed.\n ', manager=services.buff_manager())), 'buffs_to_ignore': TunableList( description= "\n If 'remove_all_buffs' is checked, no buffs included in this list will\n be removed. If 'remove_all_buffs' is unchecked, this list will be\n ignored.\n ", tunable=TunableReference( description='\n Buff to be removed.\n ', manager=services.buff_manager())) } def __init__(self, remove_all_visible_buffs, buffs_to_remove, buffs_to_ignore, **kwargs): super().__init__(**kwargs) self._remove_all_visible_buffs = remove_all_visible_buffs self._buffs_to_remove = buffs_to_remove self._buffs_to_ignore = buffs_to_ignore def _apply_to_subject_and_target(self, subject, target, resolver): removal_list = [] if self._remove_all_visible_buffs: removal_list.extend(subject.Buffs) for buff in removal_list: if type(buff) in self._buffs_to_ignore: pass if not buff.visible: pass if buff.commodity is not None: tracker = subject.get_tracker(buff.commodity) commodity_inst = tracker.get_statistic(buff.commodity) if commodity_inst.core: pass subject.Buffs.remove_buff_entry(buff) else: for buff_type in self._buffs_to_remove: subject.Buffs.remove_buff_by_type(buff_type)
def archive_buff_message(buff_msg, shows_timeout, change_rate): buff_reason = hex( buff_msg.reason.hash) if buff_msg.HasField('reason') else None entry = { 'buff_id': buff_msg.buff_id, 'equipped': buff_msg.equipped, 'buff_reason': buff_reason, 'is_mood_buff': buff_msg.is_mood_buff, 'commodity_guid': buff_msg.commodity_guid, 'transition_into_buff_id': buff_msg.transition_into_buff_id } manager = services.buff_manager() if manager: buff_cls = manager.get(buff_msg.buff_id) entry['buff_name'] = buff_cls.__name__ if buff_msg.timeout: entry['timeout'] = buff_msg.timeout entry['rate'] = buff_msg.rate_multiplier if buff_msg.equipped and shows_timeout and change_rate is not None: if buff_msg.buff_progress == Sims_pb2.BUFF_PROGRESS_NONE: entry['progress_arrow'] = 'No Arrow' elif buff_msg.buff_progress == Sims_pb2.BUFF_PROGRESS_UP: entry['progress_arrow'] = 'Arrow Up' else: entry['progress_arrow'] = 'Arrow Down' if buff_msg.HasField('mood_type_override'): entry['mood_type_override'] = buff_msg.mood_type_override sim_buff_log_archiver.archive(data=entry, object_id=buff_msg.sim_id)
class FishingBait(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'bait_name': TunableLocalizedStringFactory( description='\n Name of fishing bait.\n '), 'bait_description': TunableLocalizedStringFactory( description= '\n Description of fishing bait.\n '), 'bait_icon_definition': TunableReference( description= '\n Object definition that will be used to render icon of fishing bait.\n ', manager=services.definition_manager()), 'bait_buff': TunableReference( description='\n Buff of fishing bait.\n ', manager=services.buff_manager()), 'bait_priority': TunableRange( description= '\n The priority of the bait. When an object can be categorized into\n multiple fishing bait categories, the highest priority category \n will be chosen.\n ', tunable_type=int, default=1, minimum=1) }
class FishingLocationExamineWaterSuperInteraction( FishingLocationSuperInteraction): EXAMINE_SUCCESS_NOTIFICATION = ui.ui_dialog_notification.UiDialogNotification.TunableFactory( description= "\n The notification that is displayed when a Sim successfully examines a fishing location.\n \n Notice that the text itself can't be tuned here. Those will be pulled\n from the Examine Localization Map it a fish is found that requires\n bait, or we'll use the Generic Examine Notification Text if there are\n no fish that require bait.\n ", locked_args={'text': None}) BAIT_NOTIFICATION_TEXT_MAP = sims4.tuning.tunable.TunableMapping( key_type=sims4.tuning.tunable.TunableReference( manager=services.buff_manager(), pack_safe=True), key_name='Bait Buff', value_type=sims4.localization.TunableLocalizedStringFactory( description= '\n If the Sim examines the water and a fish in the water requires the\n tuned Bait Buff, there is a chance this is the string that will show\n up in the TNS.\n ' ), value_name='Notification Text') GENERIC_EXAMINE_NOTIFICATION_TEXT = sims4.localization.TunableLocalizedStringFactory( description= '\n If the Sim successfully examines the water but there are no fish that\n require bait, this is the string that will show in the notification.\n ' ) _notification_bait_types = singletons.EMPTY_SET @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() cls._notification_bait_types = frozenset( cls.BAIT_NOTIFICATION_TEXT_MAP) def _build_outcome_sequence(self): def end(_): if self.global_outcome_result == interactions.utils.outcome_enums.OutcomeResult.SUCCESS: self._show_success_notification() sequence = super()._build_outcome_sequence() return element_utils.build_critical_section_with_finally(sequence, end) def _decide_localized_string(self): fishing_data = self.get_fishing_data_from_target() if fishing_data is not None: required_baits = set() resolver = self.get_resolver() for fish in fishing_data.get_possible_fish_gen(): bait = fish.fish.cls.required_bait_buff if bait in self._notification_bait_types: if fish.fish.cls.can_catch(resolver): required_baits.add(bait) if required_baits: chosen_bait = random.choice(list(required_baits)) loc_string = self.BAIT_NOTIFICATION_TEXT_MAP.get(chosen_bait) return loc_string(self.sim) return self.GENERIC_EXAMINE_NOTIFICATION_TEXT(self.sim) def _show_success_notification(self): dialog = self.EXAMINE_SUCCESS_NOTIFICATION( self.sim, self.get_resolver(), text=lambda *_: self._decide_localized_string()) dialog.show_dialog()
def setup_customer(self): if self._buffs_to_load is not None: sim_info = services.sim_info_manager().get(self._sim_id) if sim_info.is_instanced(): buff_manager = services.buff_manager() for buff_id in self._buffs_to_load: buff = buff_manager.get(buff_id) if buff is not None: sim_info.add_buff(buff) self._buffs_to_load = None self._post_sim_info_loaded_init()
class ChefTuning: CHEF_STATION_POT_OBJECT = TunablePackSafeReference( description= "\n The pot object to create at the chef's station.\n ", manager=services.definition_manager()) CHEF_STATION_PAN_OBJECT = TunablePackSafeReference( description= "\n The pan object to create at the chef's station.\n ", manager=services.definition_manager()) CHEF_STATION_CUTTING_BOARD_OBJECT = TunablePackSafeReference( description= "\n The cutting board object to create at the chef's station.\n ", manager=services.definition_manager()) CHEF_STATION_PAN_SLOT = Tunable( description= '\n The name of the slot in which the pan object should be placed.\n ', tunable_type=str, default='_ctnm_SimInteraction_1') CHEF_STATION_POT_SLOT = Tunable( description= '\n The name of the slot in which the pot object should be placed.\n ', tunable_type=str, default='_ctnm_SimInteraction_2') CHEF_STATION_CUTTING_BOARD_SLOT = Tunable( description= '\n The name of the slot in which the cutting board object should be placed.\n ', tunable_type=str, default='_ctnm_SimInteraction_4') CHEF_STATION_SERVE_SLOT_TYPE = TunableReference( description= '\n The slot type of the serve slots on the chef station.\n ', manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE)) CHEF_STATION_SERVING_PLATTER_OBJECT = TunablePackSafeReference( description= "\n The serving platter object the chef will create and place when they're\n done cooking an order.\n ", manager=services.definition_manager()) CHEF_HAS_ORDER_BUFF = TunablePackSafeReference( description= '\n The buff a chef should get when they have an order. This should drive\n them to do the active cooking animations.\n ', manager=services.buff_manager()) CHEF_COMPLIMENT_LOOT = LootActions.TunablePackSafeReference( description= "\n The loot action to trigger when a customer compliments a chef. This\n won't happen until the waitstaff deliver the compliment.\n \n The customer Sim will be the Actor and the Chef will be TargetSim.\n " ) CHEF_INSULT_LOOT = LootActions.TunablePackSafeReference( description= "\n The loot action to trigger when a customer insults a chef. This won't\n happen until the waitstaff deliver the insult.\n \n The customer Sim will be the Actor and the Chef will be TargetSim.\n " ) PICK_UP_ORDER_INTERACTION = TunablePackSafeReference( description= '\n The interaction the sim will run when they pick their order up from the\n Chef Station. This is only used when a Sim places an order directly at\n the chef station.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION))
class CheatWoohooTuning: CHEAT_WOOHOO_BITS = TunableList( TunableReference(manager=services.relationship_bit_manager())) CHEAT_WOOHOO_TRACK = TunableReference( manager=services.statistic_manager(), class_restrictions=('RelationshipTrack', )) CHEAT_WOOHOO_COMMODITY = TunableReference( manager=services.statistic_manager(), class_restrictions=('Commodity', )) CHEAT_WOOHOO_BUFF = TunableReference(manager=services.buff_manager()) CHEAT_WOOHOO_SOCIALCONTEXT = TunableReference( manager=services.statistic_manager(), class_restrictions='RelationshipTrack')
def __init__(self, reload_dependent=False, **kwargs): super().__init__( buff_type=TunableReference( manager=services.buff_manager(), description='Buff that will get added to sim.', reload_dependent=reload_dependent), buff_reason=OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), **kwargs)
class SicknessTuning: SICKNESS_BUFFS_PLAYER_FACED = TunableList( description= "\n List of buffs that define if a sim is sick from what the player can \n see. The way sickness work, a sim might be sick but it may not be \n visible to the player, so on this list we should only tune the buff's\n that would make the sim sick on the players perspective.\n i.e. buffs that would make a child sim take a day of school.\n ", tunable=TunableReference(manager=services.buff_manager(), pack_safe=True)) LOOT_ACTIONS_ON_CHILD_CAREER_AUTO_SICK = TunableList( description= '\n Loot actions to test and apply on the event its time to go to work \n and the child sim is sick.\n i.e. notification... \n ', tunable=LootActions.TunableReference(pack_safe=True)) @classmethod def is_child_sim_sick(cls, sim_info): if not sim_info.is_child: return False return any( sim_info.has_buff(buff_type) for buff_type in SicknessTuning.SICKNESS_BUFFS_PLAYER_FACED)
def archive_buff_message(buff_msg, shows_timeout, change_rate): buff_reason = hex(buff_msg.reason.hash) if buff_msg.HasField('reason') else None entry = {'buff_id': buff_msg.buff_id, 'equipped': buff_msg.equipped, 'buff_reason': buff_reason, 'is_mood_buff': buff_msg.is_mood_buff, 'commodity_guid': buff_msg.commodity_guid, 'transition_into_buff_id': buff_msg.transition_into_buff_id} manager = services.buff_manager() if manager: buff_cls = manager.get(buff_msg.buff_id) entry['buff_name'] = buff_cls.__name__ if buff_msg.timeout: entry['timeout'] = buff_msg.timeout entry['rate'] = buff_msg.rate_multiplier if buff_msg.equipped and shows_timeout and change_rate is not None: if buff_msg.buff_progress == Sims_pb2.BUFF_PROGRESS_NONE: entry['progress_arrow'] = 'No Arrow' elif buff_msg.buff_progress == Sims_pb2.BUFF_PROGRESS_UP: entry['progress_arrow'] = 'Arrow Up' else: entry['progress_arrow'] = 'Arrow Down' if buff_msg.HasField('mood_type_override'): entry['mood_type_override'] = buff_msg.mood_type_override sim_buff_log_archiver.archive(data=entry, object_id=buff_msg.sim_id)
class RoyaltyTracker(SimInfoTracker): PAYMENT_SCHEDULE = WeeklySchedule.TunableFactory( description= '\n The schedule for when payments should be made. This is global to all\n sims that are receiving royalties..\n ' ) ROYALTY_TYPE_DATA = TunableMapping( key_type=TunableEnumEntry( description='\n The type of royalty.\n ', tunable_type=RoyaltyType, default=RoyaltyType.INVALID, invalid_enums=(RoyaltyType.INVALID, )), value_type=TunableTuple( description= '\n The data associated with the mapped royatly type.\n ', royalty_name=TunableLocalizedString( description= '\n The localized name of the RoyaltyType. This is how it will show up in the\n Royalty notification to the player.\n ' ), one_time_bonus=OptionalTunable( description= '\n If enabled, allows tuning a bonus to the next royalty payment if\n the Sim has the corresponding buff.\n ', tunable=TunableTuple( description= '\n Tuning that provides a bonus to a single royalty payment.\n ', bonus_buff=TunablePackSafeReference( description= '\n If the sim has this buff when a royalty payment happens,\n that payment will be multiplied by the tuned Bonus\n Multiplier. This buff will then be removed from the Sim.\n ', manager=services.buff_manager()), bonus_multiplier=TunableRange( description= '\n The amount to multiply the next royalty payment by if the\n Sim has the tuned Bonus Buff.\n ', tunable_type=float, default=2, minimum=0)))), verify_tunable_callback=_verify_tunable_callback) ROYALTY_ENTRY_ITEM = TunableLocalizedStringFactory( description= '\n The localized string for a royalty entry.\n {0.String}: {1.Money}\n ' ) ROYALTY_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification displayed when royalties are viewed.\n ', locked_args={'text': None}) def __init__(self, sim_info): self._sim_ref = weakref.ref(sim_info) self._royalties = {} @property def sim_info(self): return self._sim_ref() @property def has_royalties(self): for (_, royalty_list) in self._royalties.items(): if royalty_list: return True return False def clear_royalty_tracker(self): self._royalties.clear() def start_royalty(self, royalty_type, royalty_guid64, entry_name, multiplier, starting_payment=0): if royalty_type not in self._royalties.keys(): self._royalties[royalty_type] = [] self._royalties[royalty_type].append( Royalty(royalty_guid64, entry_name, multiplier, starting_payment)) def update_royalties_and_get_paid(self): if not self.has_royalties: return sim_info = self.sim_info if sim_info is None: logger.error( 'Trying to pay out a Sim but the Sim is None. Perhaps they died? Clearing out royalties for this Sim. Sim: {}', sim_info) self._royalties.clear() return tag_payment_map = {} royalty_payment_dict = collections.defaultdict(list) for (royalty_type, royalty_list) in self._royalties.items(): bonus_buff = None bonus_multiplier = None bonus_royalty = RoyaltyTracker.ROYALTY_TYPE_DATA.get( royalty_type).one_time_bonus if bonus_royalty is not None: bonus_buff = bonus_royalty.bonus_buff.buff_type if sim_info.Buffs.has_buff(bonus_buff): bonus_multiplier = bonus_royalty.bonus_multiplier for royalty in reversed(royalty_list): royalty_tuning = RoyaltyPayment.get_royalty_payment_tuning( royalty.royalty_guid64) if royalty_tuning is None: logger.error( 'royalty_tuning is none for sim {}. royalty: {}.', sim_info, royalty) elif royalty.update(royalty_tuning): payment_tag = royalty_tuning.payment_tag payment_amount = RoyaltyTracker.get_payment_amount( royalty, royalty_tuning) if bonus_multiplier is not None: payment_amount *= bonus_multiplier royalty_payment_dict[royalty] = payment_amount if payment_tag not in tag_payment_map: tag_payment_map[payment_tag] = 0 tag_payment_map[payment_tag] += payment_amount else: royalty_list.remove(royalty) if bonus_multiplier is not None: sim_info.Buffs.remove_buff_by_type(bonus_buff) for (payment_tag, payment_amount) in tag_payment_map.items(): tags = None if payment_tag != tag.Tag.INVALID: tags = frozenset((payment_tag, )) sim_info.household.funds.add( payment_amount, Consts_pb2.TELEMETRY_MONEY_ROYALTY, sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS), tags=tags) if royalty_payment_dict: self.show_royalty_notification(royalty_payment_dict) def show_royalty_notification(self, royalty_payment_dict): notification_text = LocalizationHelperTuning.get_new_line_separated_strings( *(LocalizationHelperTuning.get_bulleted_list( RoyaltyTracker.get_name_for_type(royalty_type), *(RoyaltyTracker.get_line_item_string(r.entry_name, royalty_payment_dict[r]) for r in royalty_list)) for (royalty_type, royalty_list) in self._royalties.items() if royalty_list)) sim_info = self.sim_info resolver = SingleSimResolver(sim_info) dialog = self.ROYALTY_NOTIFICATION(sim_info, resolver, text=lambda *_: notification_text) dialog.show_dialog() @staticmethod def get_name_for_type(royalty_type): return RoyaltyTracker.ROYALTY_TYPE_DATA.get(royalty_type).royalty_name @staticmethod def get_payment_amount(royalty, royalty_tuning): deviation_percent = royalty_tuning.payment_deviation_percent payment_amount = royalty_tuning.pay_curve.get( royalty.current_payment) * royalty.multiplier if deviation_percent == 0: return int(payment_amount) deviation = payment_amount * deviation_percent min_payment = payment_amount - deviation max_payment = payment_amount + deviation return int(sims4.random.uniform(min_payment, max_payment)) @staticmethod def get_line_item_string(name, amount): return RoyaltyTracker.ROYALTY_ENTRY_ITEM(name, amount) def save(self): data = protocols.PersistableRoyaltyTracker() for (royalty_type, royalty_list) in self._royalties.items(): for royalty in royalty_list: with ProtocolBufferRollback(data.royalties) as royalty_data: royalty_data.royalty_type = int(royalty_type) royalty_data.royalty_guid64 = royalty.royalty_guid64 royalty_data.entry_name = royalty.entry_name royalty_data.multiplier = royalty.multiplier royalty_data.current_payment = royalty.current_payment return data def load(self, data): for royalty_data in data.royalties: entry_name = Localization_pb2.LocalizedString() entry_name.MergeFrom(royalty_data.entry_name) self.start_royalty(royalty_type=RoyaltyType( royalty_data.royalty_type), royalty_guid64=royalty_data.royalty_guid64, entry_name=entry_name, multiplier=royalty_data.multiplier, starting_payment=royalty_data.current_payment) @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.FULL def on_lod_update(self, old_lod, new_lod): if new_lod < self._tracker_lod_threshold: self.clear_royalty_tracker() elif old_lod < self._tracker_lod_threshold: sim_msg = services.get_persistence_service().get_sim_proto_buff( self._sim_ref().id) if sim_msg is not None: self.load(sim_msg.attributes.royalty_tracker)
class BuffCondition(HasTunableFactory, AutoFactoryInit, Condition): __qualname__ = 'BuffCondition' class Timing(enum.Int): __qualname__ = 'BuffCondition.Timing' ON_ADD = 0 ON_REMOVE = 1 HAS_BUFF = 2 NOT_HAS_BUFF = 3 FACTORY_TUNABLES = {'description': '\n A condition that is satisfied when a Sim gains or loses a buff.\n ', 'participant': TunableEnumEntry(description="\n The participant whose buffs we're checking.\n ", tunable_type=ParticipantTypeActorTargetSim, default=ParticipantTypeActorTargetSim.Actor), 'buff': TunableReference(description="\n The buff we're checking.\n ", manager=services.buff_manager()), 'timing': TunableEnumEntry(description='\n When the condition satisfies.\n Choices:\n ON_ADD: Only check the condition on the edge of the buff being\n added. This will not satisfy if you have the buff when the\n interaction starts.\n ON_REMOVE: Only check the condition on the edge of the buff being\n removed. This will not satisfy if you do not have the buff when\n the interaction starts.\n HAS_BUFF: Check for the buff existing at any time this condition\n is active. This will satisfy if you have the buff when the\n interaction starts.\n NOT_HAS_BUFF: Check for the buff not existing at any time this\n condition is active. This will satisfy if you do not have the buff\n when the interaction starts.\n ', tunable_type=Timing, default=Timing.ON_ADD)} def __str__(self): return 'BuffCondition: {} {} {}'.format(self.participant, self.buff, self.timing) def attach_to_owner(self, owner, callback): self.si_callback = callback self._owner = owner sim = self._owner.get_participant(self.participant) if self.timing == BuffCondition.Timing.HAS_BUFF: if sim.has_buff(self.buff): self._satisfy() elif not (self.timing == BuffCondition.Timing.NOT_HAS_BUFF and sim.has_buff(self.buff)): self._satisfy() self._enable_buff_watcher() return (None, None) def detach_from_owner(self, owner, exiting=False): self._disable_buff_watcher() def _enable_buff_watcher(self): sim = self._owner.get_participant(self.participant) sim.Buffs.on_buff_added.append(self._on_buff_added) sim.Buffs.on_buff_removed.append(self._on_buff_removed) def _disable_buff_watcher(self): sim = self._owner.get_participant(self.participant) if self._on_buff_added in sim.Buffs.on_buff_added: sim.Buffs.on_buff_added.remove(self._on_buff_added) if self._on_buff_removed in sim.Buffs.on_buff_removed: sim.Buffs.on_buff_removed.remove(self._on_buff_removed) def _on_buff_added(self, buff_type): if buff_type is self.buff: if self.timing == BuffCondition.Timing.ON_ADD or self.timing == BuffCondition.Timing.HAS_BUFF: self._satisfy() else: self._unsatisfy() def _on_buff_removed(self, buff_type): if buff_type is self.buff: if self.timing == BuffCondition.Timing.ON_REMOVE or self.timing == BuffCondition.Timing.NOT_HAS_BUFF: self._satisfy() else: self._unsatisfy()
class RemoveBuffLiability(Liability, HasTunableFactory): LIABILITY_TOKEN = 'RemoveBuffLiability' FACTORY_TUNABLES = {'buff_to_remove': TunablePackSafeReference(description='\n The buff to remove on the interaction finishing.\n ', manager=services.buff_manager())} def __init__(self, interaction, buff_to_remove, **kwargs): super().__init__(**kwargs) self._sim_info = interaction.sim.sim_info self._buffs_to_remove = set() if buff_to_remove is not None: self._buffs_to_remove.add(buff_to_remove) def merge(self, interaction, key, new_liability): new_liability._buffs_to_remove.update(self._buffs_to_remove) return new_liability def release(self): for buff_type in self._buffs_to_remove: self._sim_info.remove_buff_by_type(buff_type)
class Region(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.region_manager()): REGION_DESCRIPTION_TUNING_MAP = TunableMapping( description= '\n A mapping between Catalog region description and tuning instance. This\n way we can find out what region description the current zone belongs to\n at runtime then grab its tuning instance.\n ', key_type=TunableRegionDescription( description= '\n Catalog-side Region Description.\n ', pack_safe=True, export_modes=ExportModes.All), value_type=TunableReference( description= "\n Region Tuning instance. This is retrieved at runtime based on what\n the active zone's region description is.\n ", pack_safe=True, manager=services.region_manager(), export_modes=ExportModes.All), key_name='RegionDescription', value_name='Region', tuple_name='RegionDescriptionMappingTuple', export_modes=ExportModes.All) INSTANCE_TUNABLES = { 'gallery_download_venue_map': TunableMapping( description= '\n A map from gallery venue to instanced venue. We need to be able to\n convert gallery venues into other venues that are only compatible\n with that region.\n ', key_type=TunableReference( description= '\n A venue type that exists in the gallery.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), value_type=TunableReference( description= '\n The venue type that the gallery venue will become when it is\n downloaded into this region.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), key_name='gallery_venue_type', value_name='region_venue_type', tuple_name='GalleryDownloadVenueMappingTuple', export_modes=ExportModes.All), 'compatible_venues': TunableList( description= '\n A list of venues that are allowed to be set by the player in this\n region.\n ', tunable=TunableReference( description= '\n A venue that the player can set in this region.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), export_modes=ExportModes.All), 'tags': TunableList( description= '\n Tags that are used to group regions. Destination Regions will\n likely have individual tags, but Home/Residential Regions will\n share a tag.\n ', tunable=TunableEnumEntry( description= '\n A Tag used to group this region. Destination Regions will\n likely have individual tags, but Home/Residential Regions will\n share a tag.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True)), 'region_buffs': TunableList( description= '\n A list of buffs that are added on Sims while they are instanced in\n this region.\n ', tunable=TunableReference( description= '\n A buff that exists on Sims while they are instanced in this\n region.\n ', manager=services.buff_manager(), pack_safe=True)), 'store_travel_group_placed_objects': Tunable( description= '\n If checked, any placed objects while in a travel group will be returned to household inventory once\n travel group is disbanded.\n ', tunable_type=bool, default=False), 'travel_group_build_disabled_tooltip': TunableLocalizedString( description= '\n The string that will appear in the tooltip of the grayed out build\n mode button if build is being disabled because of a travel group in\n this region.\n ', allow_none=True, export_modes=ExportModes.All), 'sunrise_time': TunableTimeOfDay( description= '\n The time, in Sim-time, the sun rises in this region.\n ', default_hour=6, tuning_group=GroupNames.TIME), 'seasonal_sunrise_time': TunableMapping( description= '\n A mapping between season and sunrise time. If the current season\n is not found then we will default to the tuned sunrise time.\n ', key_type=TunableEnumEntry( description='\n The season.\n ', tunable_type=SeasonType, default=SeasonType.SUMMER), value_type=TunableTimeOfDay( description= '\n The time, in Sim-time, the sun rises in this region, in this\n season.\n ', default_hour=6, tuning_group=GroupNames.TIME)), 'sunset_time': TunableTimeOfDay( description= '\n The time, in Sim-time, the sun sets in this region.\n ', default_hour=20, tuning_group=GroupNames.TIME), 'seasonal_sunset_time': TunableMapping( description= '\n A mapping between season and sunset time. If the current season\n is not found then we will default to the tuned sunset time.\n ', key_type=TunableEnumEntry( description='\n The season.\n ', tunable_type=SeasonType, default=SeasonType.SUMMER), value_type=TunableTimeOfDay( description= '\n The time, in Sim-time, the sun sets in this region, in this\n season.\n ', default_hour=20, tuning_group=GroupNames.TIME)), 'provides_sunlight': Tunable( description= '\n If enabled, this region provides sunlight between the tuned Sunrise\n Time and Sunset Time. This is used for gameplay effect (i.e.\n Vampires).\n ', tunable_type=bool, default=True, tuning_group=GroupNames.TIME), 'weather': TunableMapping( description= '\n Forecasts for this region for the various seasons\n ', key_type=TunableEnumEntry( description='\n The Season.\n ', tunable_type=SeasonType, default=SeasonType.SPRING), value_type=TunableWeatherSeasonalForecastsReference( description= '\n The forecasts for the season by part of season\n ', pack_safe=True)), 'weather_supports_fresh_snow': Tunable( description= '\n If enabled, this region supports fresh snow.\n ', tunable_type=bool, default=True), 'seasonal_parameters': TunableMapping( description='\n ', key_type=TunableEnumEntry( description= '\n The parameter that we wish to change.\n ', tunable_type=SeasonParameters, default=SeasonParameters.LEAF_ACCUMULATION), value_type=TunableList( description= '\n A list of the different seasonal parameter changes that we want to\n send over the course of a year.\n ', tunable=TunableTuple( season=TunableEnumEntry( description= '\n The Season that this change is in.\n ', tunable_type=SeasonType, default=SeasonType.SPRING), time_in_season=TunableRange( description= '\n The time within the season that this will occur.\n ', tunable_type=float, minimum=0.0, maximum=1.0, default=0.0), value=Tunable( description= '\n The value that we will set this parameter to in the\n season\n ', tunable_type=float, default=0.0))), verify_tunable_callback=verify_seasonal_parameters), 'fishing_data': OptionalTunable( description= '\n If enabled, define all of the data for fishing locations in this region.\n Only used if objects are tuned to use region fishing data.\n ', tunable=TunableFishingDataSnippet()), 'welcome_wagon_replacement': OptionalTunable( description= '\n If enabled then we will replace the Welcome Wagon with a new situation.\n \n If the narrative is also set to replace the welcome wagon that will take precedent over this replacement.\n ', tunable=TunableReference( description= '\n The situation we will use to replace the welcome wagon.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION))) } @classmethod def _cls_repr(cls): return "Region: <class '{}.{}'>".format(cls.__module__, cls.__name__) @classmethod def is_region_compatible(cls, region_instance, ignore_tags=False): if region_instance is cls or region_instance is None: return True if ignore_tags: return False for tag in cls.tags: if tag in region_instance.tags: return True return False @classmethod def is_sim_info_compatible(cls, sim_info): other_region = get_region_instance_from_zone_id(sim_info.zone_id) if cls.is_region_compatible(other_region): return True else: travel_group_id = sim_info.travel_group_id if travel_group_id: travel_group = services.travel_group_manager().get( travel_group_id) if travel_group is not None and not travel_group.played: return True return False @classmethod def get_sunrise_time(cls): season_service = services.season_service() if season_service is None: return cls.sunrise_time return cls.seasonal_sunrise_time.get(season_service.season, cls.sunrise_time) @classmethod def get_sunset_time(cls): season_service = services.season_service() if season_service is None: return cls.sunset_time return cls.seasonal_sunset_time.get(season_service.season, cls.sunset_time)
class DynamicBuffLootOp(BaseLootOperation): __qualname__ = 'DynamicBuffLootOp' @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, value): if value.subject == ParticipantType.All: logger.error( 'Tuning error: DynamicBuffLootOp should not be subject All. Use AllSims:{}, {}', instance_class, tunable_name) elif value.subject == ParticipantType.PickedItemId: logger.error( 'Tuning error: DynamicBuffLootOp should not be subject PickedItemId, use PickedSim:{}, {}', instance_class, tunable_name) FACTORY_TUNABLES = { 'description': '\n This loot will give a random buff based on the weight get tuned inside.\n ', 'buffs': TunableMapping( description='\n ', key_type=TunableReference( description= '\n Buff that will get this weight in the random.', manager=services.buff_manager()), value_type=Tunable( description='\n The weight value.', tunable_type=float, default=0)), 'buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'verify_tunable_callback': _verify_tunable_callback } def __init__(self, buffs, buff_reason, **kwargs): super().__init__(**kwargs) self._buffs = buffs self._buff_reason = buff_reason self._random_buff = None def _get_random_buff(self): if self._random_buff is None: buff_pair_list = list(self._buffs.items()) self._random_buff = sims4.random.pop_weighted(buff_pair_list, flipped=True) return self._random_buff def _apply_to_subject_and_target(self, subject, target, resolver): random_buff = self._get_random_buff() if random_buff is not None: if not subject.is_sim: logger.error( 'Tuning error: subject {} of DynamicBuffLootOp giving buff {} for reason {} is not a sim', self.subject, random_buff, self._buff_reason) return subject.add_buff_from_op(random_buff, self._buff_reason) def _on_apply_completed(self): random_buff = self._random_buff self._random_buff = None return random_buff
class CurfewService(Service): ALLOWED_CURFEW_TIMES = TunableList( description= '\n A list of times (in military time) that are allowed to be set as curfew\n times.\n \n NOTE: Many objects will have curfew components and will only support\n a visual of certain values. Changing these values without making sure\n the art supports the value will not work properly. Please only change\n these values if you know for sure they need to be changed and are \n getting support from modelling to make the change.\n ', tunable=TunableRange( description= '\n The hour for which the curfew will be set to.\n ', tunable_type=int, default=0, minimum=0, maximum=23)) CURFEW_END_TIME = TunableRange( description= '\n The time when the curfew is considered to be over and the Sims are \n no longer subject to it.\n \n This should probably be set to some time in the morning. 6am perhaps.\n ', tunable_type=int, default=0, minimum=0, maximum=23) MINUTES_BEFORE_CURFEW_WARNING = TunableSimMinute( description= '\n The minutes before the curfew starts that a Sim should receive a \n warning about the curfew being about to start.\n ', default=30) BREAK_CURFEW_WARNING = TunableLocalizedStringFactory( description= '\n The string that is used to warn the player that a pie menu action will\n cause the Sim to break curfew. This will wrap around the name of the \n interaction so should be tuned to something like [Warning] {0.String}.\n ' ) CURFEW_WARNING_TEXT_MESSAGE_DIALOG = UiDialogOk.TunableFactory( description= '\n The dialog to display as a text message when warning a Sim that their\n curfew is about to expire.\n ' ) CURFEW_WARNING_SIM_TESTS = TunableTestSet( description= '\n Tests to run on each of the Sims to determine if they should receive\n the curfew warning text message or not.\n ' ) BREAK_CURFEW_BUFF = TunablePackSafeReference( description= "\n The buff that get's added to a Sim that breaks curfew. This buff will\n enable the Sim to be disciplined for their behavior.\n ", manager=services.buff_manager()) INTERACTION_BLACKLIST_TAGS = TunableSet( description= '\n A list of all the tags that blacklist interactions from causing Sims to\n break curfew.\n ', tunable=TunableEnumEntry( description= '\n A tag that when tagged on the interaction will allow the Sim to run\n the interaction and not break curfew.\n ', tunable_type=Tag, default=Tag.INVALID, pack_safe=True)) CURFEW_BEGIN_LOOT = TunablePackSafeReference( description= '\n The loot to apply to all Sims in the family when curfew begins. This\n will allow us to give buffs that affect the behavior of the Sims if\n they pass certain tests.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION)) CURFEW_END_LOOT = TunablePackSafeReference( description= '\n The loot to apply to all Sims in the family when curfew ends. This will\n allow us to remove buffs that affect the behavior of the Sims.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION)) UNSET = -1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._zone_curfew_data = {} self._curfew_warning_alarm_handle = None self._curfew_started_alarm_handle = None self._curfew_ended_alarm_handle = None self._curfew_message_alarm_handle = None self._curfew_warning_callback = CallableList() self._curfew_started_callback = CallableList() self._curfew_ended_callback = CallableList() self._time_set_callback = CallableList() def get_zone_curfew(self, zone_id): curfew_setting = self._zone_curfew_data.get(zone_id, self.UNSET) return curfew_setting def set_zone_curfew(self, zone_id, curfew_setting): if self._zone_curfew_data.get(zone_id, None) == curfew_setting: return if curfew_setting not in CurfewService.ALLOWED_CURFEW_TIMES and curfew_setting != CurfewService.UNSET: return self._zone_curfew_data[zone_id] = curfew_setting self._update_curfew_settings(zone_id, curfew_setting) self._setup_curfew_text_message() def _update_curfew_settings(self, current_zone_id, current_setting): self._create_alarm_handles(current_zone_id) self._time_set_callback(current_setting) def _create_alarm_handles(self, zone_id): for alarm in (self._curfew_warning_alarm_handle, self._curfew_started_alarm_handle, self._curfew_ended_alarm_handle): if alarm is not None: alarms.cancel_alarm(alarm) time = self._zone_curfew_data.get(zone_id, self.UNSET) now = services.time_service().sim_now self._create_warning_callback(now, time) self._create_curfew_callback(now, time) self._create_curfew_ended_callback(now, time) def _create_warning_callback(self, now, time): if time is not CurfewService.UNSET: alarm_time = date_and_time.create_date_and_time(hours=time - 1) warning_span = now.time_till_next_day_time(alarm_time) if warning_span.in_ticks() == 0: warning_span += TimeSpan(date_and_time.sim_ticks_per_day()) self._curfew_warning_alarm_handle = alarms.add_alarm( self, warning_span, self._handle_warning_callback, False) def _handle_warning_callback(self, handle): self._curfew_warning_callback() now = services.time_service().sim_now time = self._zone_curfew_data.get(services.current_zone_id(), CurfewService.UNSET) self._create_warning_callback(now, time) def _create_curfew_callback(self, now, time): if time is not self.UNSET: alarm_time = date_and_time.create_date_and_time(hours=time) curfew_span = now.time_till_next_day_time(alarm_time) if curfew_span.in_ticks() == 0: curfew_span += TimeSpan(date_and_time.sim_ticks_per_day()) self._curfew_started_alarm_handle = alarms.add_alarm( self, curfew_span, self._handle_curfew_callback, False) def _handle_curfew_callback(self, handle): self._curfew_started_callback() now = services.time_service().sim_now time = self._zone_curfew_data.get(services.current_zone_id(), CurfewService.UNSET) self.apply_curfew_loots() self._create_curfew_callback(now, time) def _create_curfew_ended_callback(self, now, time): if time is not CurfewService.UNSET: alarm_time = date_and_time.create_date_and_time( hours=CurfewService.CURFEW_END_TIME) curfew_span = now.time_till_next_day_time(alarm_time) if curfew_span.in_ticks() == 0: curfew_span += TimeSpan(date_and_time.sim_ticks_per_day()) self._curfew_ended_alarm_handle = alarms.add_alarm( self, curfew_span, self._handle_curfew_ended_callback, False) def _handle_curfew_ended_callback(self, handle): self._curfew_ended_callback() now = services.time_service().sim_now time = CurfewService.CURFEW_END_TIME self.remove_curfew_loots() self._create_curfew_ended_callback(now, time) def register_for_alarm_callbacks(self, warning_callback, curfew_callback, curfew_over_callback, time_set_callback): self._curfew_warning_callback.append(warning_callback) self._curfew_started_callback.append(curfew_callback) self._curfew_ended_callback.append(curfew_over_callback) self._time_set_callback.append(time_set_callback) def unregister_for_alarm_callbacks(self, warning_callback, curfew_callback, curfew_over_callback, time_set_callback): if warning_callback in self._curfew_warning_callback: self._curfew_warning_callback.remove(warning_callback) if curfew_callback in self._curfew_started_callback: self._curfew_started_callback.remove(curfew_callback) if curfew_over_callback in self._curfew_ended_callback: self._curfew_ended_callback.remove(curfew_over_callback) if time_set_callback in self._time_set_callback: self._time_set_callback.remove(time_set_callback) def sim_breaking_curfew(self, sim, target, interaction=None): if interaction is not None and self.interaction_blacklisted( interaction): return False if sim.sim_info.is_in_travel_group(): return False situation_manager = services.get_zone_situation_manager() sim_situations = situation_manager.get_situations_sim_is_in(sim) if any(situation.disallows_curfew_violation for situation in sim_situations): return False active_household = services.active_household() if active_household is None: return False home_zone_id = active_household.home_zone_id curfew_setting = self._zone_curfew_data.get(home_zone_id, CurfewService.UNSET) if sim.sim_info not in active_household: return False if curfew_setting is not CurfewService.UNSET: if sim.sim_info.is_young_adult_or_older: return False elif self.past_curfew(curfew_setting): if not services.current_zone_id() == home_zone_id: ensemble_service = services.ensemble_service() ensemble = ensemble_service.get_visible_ensemble_for_sim( sim) if ensemble is not None and any( sim.sim_info.is_young_adult_or_older and sim.sim_info in active_household for sim in ensemble): return False return True if target is not None and not target.is_in_inventory( ) and not services.active_lot().is_position_on_lot( target.position): return True elif target is None and not services.active_lot( ).is_position_on_lot(sim.position): return True return True if target is not None and not target.is_in_inventory( ) and not services.active_lot().is_position_on_lot( target.position): return True elif target is None and not services.active_lot( ).is_position_on_lot(sim.position): return True return False def interaction_blacklisted(self, interaction): interaction_tags = interaction.get_category_tags() for tag in CurfewService.INTERACTION_BLACKLIST_TAGS: if tag in interaction_tags: return True return False def past_curfew(self, curfew_setting): now = services.time_service().sim_now if now.hour() >= curfew_setting or now.hour( ) < CurfewService.CURFEW_END_TIME: return True return False def _setup_curfew_text_message(self): if self._curfew_message_alarm_handle is not None: self._curfew_message_alarm_handle.cancel() self._curfew_message_alarm_handle = None current_household = services.active_household() if current_household is None: return home_zone_id = current_household.home_zone_id curfew_setting = self._zone_curfew_data.get(home_zone_id, CurfewService.UNSET) if curfew_setting is CurfewService.UNSET: return now = services.time_service().sim_now alarm_time = date_and_time.create_date_and_time(hours=curfew_setting) time_till_alarm = now.time_till_next_day_time(alarm_time) span = date_and_time.create_time_span( minutes=CurfewService.MINUTES_BEFORE_CURFEW_WARNING) time_till_alarm -= span self._curfew_message_alarm_handle = alarms.add_alarm( self, time_till_alarm, self._handle_curfew_message_callback, False) def _handle_curfew_message_callback(self, handle): active_lot = services.active_lot() if active_lot.lot_id != services.active_household_lot_id(): from_sim = None for sim_info in services.active_household(): if sim_info.is_young_adult_or_older: if not sim_info.is_instanced(): from_sim = sim_info break if from_sim is None: return for sim_info in services.active_household(): if sim_info.get_sim_instance() is None: continue resolver = DoubleSimResolver(sim_info, from_sim) if not CurfewService.CURFEW_WARNING_SIM_TESTS.run_tests( resolver): continue dialog = self.CURFEW_WARNING_TEXT_MESSAGE_DIALOG( sim_info, target_sim_id=from_sim.id, resolver=resolver) dialog.show_dialog() def add_broke_curfew_buff(self, sim): if not sim.has_buff(CurfewService.BREAK_CURFEW_BUFF): sim.add_buff(CurfewService.BREAK_CURFEW_BUFF) def remove_broke_curfew_buff(self, sim): if sim.has_buff(CurfewService.BREAK_CURFEW_BUFF): sim.remove_buff_by_type(CurfewService.BREAK_CURFEW_BUFF) def is_curfew_active_on_lot_id(self, lot_id): curfew_setting = self._zone_curfew_data.get(lot_id, CurfewService.UNSET) if curfew_setting == CurfewService.UNSET: return False return self.past_curfew(curfew_setting) def apply_curfew_loots(self): for sim_info in services.active_household(): resolver = SingleSimResolver(sim_info) CurfewService.CURFEW_BEGIN_LOOT.apply_to_resolver(resolver) def remove_curfew_loots(self): for sim_info in services.active_household(): resolver = SingleSimResolver(sim_info) CurfewService.CURFEW_END_LOOT.apply_to_resolver(resolver) @classproperty def save_error_code(cls): return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_CURFEW_SERVICE def save(self, object_list=None, zone_data=None, open_street_data=None, store_travel_group_placed_objects=False, save_slot_data=None): persistence_service = services.get_persistence_service() for save_zone_data in persistence_service.zone_proto_buffs_gen(): setting = self._zone_curfew_data.get(save_zone_data.zone_id, CurfewService.UNSET) save_zone_data.gameplay_zone_data.curfew_setting = setting def load(self, zone_data=None): persistence_service = services.get_persistence_service() for zone_data in persistence_service.zone_proto_buffs_gen(): self._zone_curfew_data[ zone_data. zone_id] = zone_data.gameplay_zone_data.curfew_setting def on_zone_load(self): current_zone_id = services.current_zone_id() self._setup_curfew_text_message() self._create_alarm_handles(current_zone_id) venue_manager = services.get_instance_manager( sims4.resources.Types.VENUE) current_venue_tuning = venue_manager.get( build_buy.get_current_venue(current_zone_id)) if current_venue_tuning.is_residential or current_venue_tuning.is_university_housing: current_setting = self._zone_curfew_data.get( current_zone_id, CurfewService.UNSET) self._update_curfew_settings(current_zone_id, current_setting) else: self._update_curfew_settings(current_zone_id, CurfewService.UNSET)
class Trait(HasTunableReference, SuperAffordanceProviderMixin, TargetSuperAffordanceProviderMixin, HasTunableLodMixin, MixerActorMixin, MixerProviderMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.trait_manager()): EQUIP_SLOT_NUMBER_MAP = TunableMapping( description= '\n The number of personality traits available to Sims of specific ages.\n ', key_type=TunableEnumEntry( description="\n The Sim's age.\n ", tunable_type=sim_info_types.Age, default=sim_info_types.Age.YOUNGADULT), value_type=Tunable( description= '\n The number of personality traits available to a Sim of the specified\n age.\n ', tunable_type=int, default=3), key_name='Age', value_name='Slot Number') PERSONALITY_TRAIT_TAG = TunableEnumEntry( description= '\n The tag that marks a trait as a personality trait.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) DAY_NIGHT_TRACKING_BUFF_TAG = TunableEnumWithFilter( description= '\n The tag that marks buffs as opting in to Day Night Tracking on traits..\n ', tunable_type=tag.Tag, filter_prefixes=['buff'], default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) INSTANCE_TUNABLES = { 'trait_type': TunableEnumEntry( description='\n The type of the trait.\n ', tunable_type=TraitType, default=TraitType.PERSONALITY, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'display_name': TunableLocalizedStringFactory( description= "\n The trait's display name. This string is provided with the owning\n Sim as its only token.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'display_name_gender_neutral': TunableLocalizedString( description= "\n The trait's gender-neutral display name. This string is not provided\n any tokens, and thus can't rely on context to properly form\n masculine and feminine forms.\n ", allow_none=True, tuning_group=GroupNames.APPEARANCE), 'trait_description': TunableLocalizedStringFactory( description="\n The trait's description.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'trait_origin_description': TunableLocalizedString( description= "\n A description of how the Sim obtained this trait. Can be overloaded\n for other uses in certain cases:\n - When the trait type is AGENT this string is the name of the \n agency's Trade type and will be provided with the owning sim \n as its token.\n - When the trait type is HIDDEN and the trait is used by the CAS\n STORIES flow, this can be used as a secondary description in \n the CAS Stories UI. If this trait is tagged as a CAREER CAS \n stories trait, this description will be used to explain which \n skills are also granted with this career.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'icon': TunableResourceKey( description="\n The trait's icon.\n ", allow_none=True, resource_types=CompoundTypes.IMAGE, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'pie_menu_icon': TunableResourceKey( description= "\n The trait's pie menu icon.\n ", resource_types=CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.APPEARANCE), 'trait_asm_overrides': TunableTuple( description= '\n Tunables that will specify if a Trait will add any parameters\n to the Sim and how it will affect their boundary conditions.\n ', param_type=OptionalTunable( description= '\n Define if this trait is parameterized as an on/off value or as\n part of an enumeration.\n ', tunable=Tunable( description= '\n The name of the parameter enumeration. For example, if this\n value is tailType, then the tailType actor parameter is set\n to the value specified in param_value, for this Sim.\n ', tunable_type=str, default=None), disabled_name='boolean', enabled_name='enum'), trait_asm_param=Tunable( description= "\n The ASM parameter for this trait. If unset, it will be auto-\n generated depending on the instance name (e.g. 'trait_Clumsy').\n ", tunable_type=str, default=None), consider_for_boundary_conditions=Tunable( description= '\n If enabled the trait_asm_param will be considered when a Sim\n is building the goals and validating against its boundary\n conditions.\n This should ONLY be enabled, if we need this parameter for\n cases like a posture transition, or boundary specific cases. \n On regular cases like an animation outcome, this is not needed.\n i.e. Vampire trait has an isVampire parameter set to True, so\n when animatin out of the coffin it does different get in/out \n animations. When this is enabled, isVampire will be set to \n False for every other Sim.\n ', tunable_type=bool, default=False), tuning_group=GroupNames.ANIMATION), 'ages': TunableSet( description= '\n The allowed ages for this trait. If no ages are specified, then all\n ages are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Age, default=None, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'genders': TunableSet( description= '\n The allowed genders for this trait. If no genders are specified,\n then all genders are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Gender, default=None, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'species': TunableSet( description= '\n The allowed species for this trait. If not species are specified,\n then all species are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID, ), export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'conflicting_traits': TunableList( description= '\n Conflicting traits for this trait. If the Sim has any of the\n specified traits, then they are not allowed to be equipped with this\n one.\n \n e.g.\n Family Oriented conflicts with Hates Children, and vice-versa.\n ', tunable=TunableReference(manager=services.trait_manager(), pack_safe=True), export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'is_npc_only': Tunable( description= '\n If checked, this trait will get removed from Sims that have a home\n when the zone is loaded or whenever they switch to a household that\n has a home zone.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'cas_selected_icon': TunableResourceKey( description= '\n Icon to be displayed in CAS when this trait has already been applied\n to a Sim.\n ', resource_types=CompoundTypes.IMAGE, default=None, allow_none=True, export_modes=(ExportModes.ClientBinary, ), tuning_group=GroupNames.CAS), 'cas_idle_asm_key': TunableInteractionAsmResourceKey( description= '\n The ASM to use for the CAS idle.\n ', default=None, allow_none=True, category='asm', export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'cas_idle_asm_state': Tunable( description= '\n The state to play for the CAS idle.\n ', tunable_type=str, default=None, source_location='cas_idle_asm_key', source_query=SourceQueries.ASMState, export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'cas_trait_asm_param': Tunable( description= '\n The ASM parameter for this trait for use with CAS ASM state machine,\n driven by selection of this Trait, i.e. when a player selects the a\n romantic trait, the Flirty ASM is given to the state machine to\n play. The name tuned here must match the animation state name\n parameter expected in Swing.\n ', tunable_type=str, default=None, export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'tags': TunableList( description= "\n The associated categories of the trait. Need to distinguish among\n 'Personality Traits', 'Achievement Traits' and 'Walkstyle\n Traits'.\n ", tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID), export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'sim_info_fixup_actions': TunableList( description= '\n A list of fixup actions which will be performed on a sim_info with\n this trait when it is loaded.\n ', tunable=TunableVariant( career_fixup_action=_SimInfoCareerFixupAction.TunableFactory( description= '\n A fix up action to set a career with a specific level.\n ' ), skill_fixup_action=_SimInfoSkillFixupAction.TunableFactory( description= '\n A fix up action to set a skill with a specific level.\n ' ), unlock_fixup_action=_SimInfoUnlockFixupAction.TunableFactory( description= '\n A fix up action to unlock certain things for a Sim\n ' ), perk_fixup_action=_SimInfoPerkFixupAction.TunableFactory( description= '\n A fix up action to grant perks to a Sim. It checks perk required\n unlock tuning and unlocks prerequisite perks first.\n ' ), default='career_fixup_action'), tuning_group=GroupNames.CAS), 'sim_info_fixup_actions_timing': TunableEnumEntry( description= "\n This is DEPRECATED, don't tune this field. We usually don't do trait-based\n fixup unless it's related to CAS stories. We keep this field only for legacy\n support reason.\n \n This is mostly to optimize performance when applying fix-ups to\n a Sim. We ideally would not like to spend time scanning every Sim \n on every load to see if they need fixups. Please be sure you \n consult a GPE whenever you are creating fixup tuning.\n ", tunable_type=SimInfoFixupActionTiming, default=SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD, tuning_group=GroupNames.DEPRECATED, deprecated=True), 'teleport_style_interaction_to_inject': TunableReference( description= '\n When this trait is added to a Sim, if a teleport style interaction\n is specified, any time another interaction runs, we may run this\n teleport style interaction to shorten or replace the route to the \n target.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), class_restrictions=('TeleportStyleSuperInteraction', ), allow_none=True, tuning_group=GroupNames.SPECIAL_CASES), 'interactions': OptionalTunable( description= '\n Mixer interactions that are available to Sims equipped with this\n trait.\n ', tunable=ContentSet.TunableFactory(locked_args={ 'phase_affordances': frozendict(), 'phase_tuning': None })), 'buffs_add_on_spawn_only': Tunable( description= '\n If unchecked, buffs are added to the Sim as soon as this trait is\n added. If checked, buffs will be added only when the Sim is\n instantiated and removed when the Sim uninstantiates.\n \n General guidelines: If the buffs only matter to Sims, for example\n buffs that alter autonomy behavior or walkstyle, this should be\n checked.\n ', tunable_type=bool, default=True), 'buffs': TunableList( description= '\n Buffs that should be added to the Sim whenever this trait is\n equipped.\n ', tunable=TunableBuffReference(pack_safe=True), unique_entries=True), 'buffs_proximity': TunableList( description= '\n Proximity buffs that are active when this trait is equipped.\n ', tunable=TunableReference(manager=services.buff_manager())), 'buff_replacements': TunableMapping( description= '\n A mapping of buff replacement. If Sim has this trait on, whenever he\n get the buff tuned in the key of the mapping, it will get replaced\n by the value of the mapping.\n ', key_type=TunableReference( description= '\n Buff that will get replaced to apply on Sim by this trait.\n ', manager=services.buff_manager(), reload_dependent=True, pack_safe=True), value_type=TunableTuple( description= '\n Data specific to this buff replacement.\n ', buff_type=TunableReference( description= '\n Buff used to replace the buff tuned as key.\n ', manager=services.buff_manager(), reload_dependent=True, pack_safe=True), buff_reason=OptionalTunable( description= '\n If enabled, override the buff reason.\n ', tunable=TunableLocalizedString( description= '\n The overridden buff reason.\n ' )), buff_replacement_priority=TunableEnumEntry( description= "\n The priority of this buff replacement, relative to other\n replacements. Tune this to be a higher value if you want\n this replacement to take precedence.\n \n e.g.\n (NORMAL) trait_HatesChildren (buff_FirstTrimester -> \n buff_FirstTrimester_HatesChildren)\n (HIGH) trait_Male (buff_FirstTrimester -> \n buff_FirstTrimester_Male)\n \n In this case, both traits have overrides on the pregnancy\n buffs. However, we don't want males impregnated by aliens\n that happen to hate children to lose their alien-specific\n buffs. Therefore we tune the male replacement at a higher\n priority.\n ", tunable_type=TraitBuffReplacementPriority, default=TraitBuffReplacementPriority.NORMAL))), 'excluded_mood_types': TunableList( TunableReference( description= '\n List of moods that are prevented by having this trait.\n ', manager=services.mood_manager())), 'outfit_replacements': TunableMapping( description= "\n A mapping of outfit replacements. If the Sim has this trait, outfit\n change requests are intercepted to produce the tuned result. If\n multiple traits with outfit replacements exist, the behavior is\n undefined.\n \n Tuning 'Invalid' as a key acts as a fallback and applies to all\n reasons.\n \n Tuning 'Invalid' as a value keeps a Sim in their current outfit.\n ", key_type=TunableEnumEntry(tunable_type=OutfitChangeReason, default=OutfitChangeReason.Invalid), value_type=TunableEnumEntry(tunable_type=OutfitChangeReason, default=OutfitChangeReason.Invalid)), 'disable_aging': OptionalTunable( description= '\n If enabled, aging out of specific ages can be disabled.\n ', tunable=TunableTuple( description= '\n The tuning that disables aging out of specific age groups.\n ', allowed_ages=TunableSet( description= '\n A list of ages that the Sim CAN age out of. If an age is in\n this list then the Sim is allowed to age out of it. If an\n age is not in this list than a Sim is not allowed to age out\n of it. For example, if the list only contains Child and\n Teen, then a Child Sim would be able to age up to Teen and\n a Teen Sim would be able to age up to Young Adult. But, a\n Young Adult, Adult, or Elder Sim would not be able to age\n up.\n ', tunable=TunableEnumEntry(Age, default=Age.ADULT)), tooltip=OptionalTunable( description= '\n When enabled, this tooltip will be displayed in the aging\n progress bar when aging is disabled because of the trait.\n ', tunable=TunableLocalizedStringFactory( description= '\n The string that displays in the aging UI when aging up\n is disabled due to the trait.\n ' ))), tuning_group=GroupNames.SPECIAL_CASES), 'can_die': Tunable( description= '\n When set, Sims with this trait are allowed to die. When unset, Sims\n are prevented from dying.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'culling_behavior': TunableVariant( description= '\n The culling behavior of a Sim with this trait.\n ', default_behavior=CullingBehaviorDefault.TunableFactory(), immune_to_culling=CullingBehaviorImmune.TunableFactory(), importance_as_npc_score=CullingBehaviorImportanceAsNpc. TunableFactory(), default='default_behavior', tuning_group=GroupNames.SPECIAL_CASES), 'always_send_test_event_on_add': Tunable( description= '\n If checked, will send out a test event when added to a trait\n tracker even if the receiving sim is hidden or not instanced.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), 'voice_effect': OptionalTunable( description= '\n The voice effect of a Sim with this trait. This is prioritized\n against other traits with voice effects.\n \n The Sim may only have one voice effect at a time.\n ', tunable=VoiceEffectRequest.TunableFactory()), 'plumbbob_override': OptionalTunable( description= '\n If enabled, allows a new plumbbob model to be used when a Sim has\n this occult type.\n ', tunable=PlumbbobOverrideRequest.TunableFactory()), 'vfx_mask': OptionalTunable( description= '\n If enabled when this trait is added the masks will be applied to\n the Sim affecting the visibility of specific VFX.\n Example: TRAIT_CHILDREN will provide a mask MASK_CHILDREN which \n the monster battle object will only display VFX for any Sim \n using that mask.\n ', tunable=TunableEnumFlags( description= "\n Mask that will be added to the Sim's mask when the trait is\n added.\n ", enum_type=VFXMask), enabled_name='apply_vfx_mask', disabled_name='no_vfx_mask'), 'day_night_tracking': OptionalTunable( description= "\n If enabled, allows this trait to track various aspects of day and\n night via buffs on the owning Sim.\n \n For example, if this is enabled and the Sunlight Buff is tuned with\n buffs, the Sim will get the buffs added every time they're in\n sunlight and removed when they're no longer in sunlight.\n ", tunable=DayNightTracking.TunableFactory()), 'persistable': Tunable( description= '\n If checked then this trait will be saved onto the sim. If\n unchecked then the trait will not be saved.\n Example unchecking:\n Traits that are applied for the sim being in the region.\n ', tunable_type=bool, default=True), 'initial_commodities': TunableSet( description= '\n A list of commodities that will be added to a sim on load, if the\n sim has this trait.\n \n If a given commodity is also blacklisted by another trait that the\n sim also has, it will NOT be added.\n \n Example:\n Adult Age Trait adds Hunger.\n Vampire Trait blacklists Hunger.\n Hunger will not be added.\n ', tunable=Commodity.TunableReference(pack_safe=True)), 'initial_commodities_blacklist': TunableSet( description= "\n A list of commodities that will be prevented from being\n added to a sim that has this trait.\n \n This always takes priority over any commodities listed in any\n trait's initial_commodities.\n \n Example:\n Adult Age Trait adds Hunger.\n Vampire Trait blacklists Hunger.\n Hunger will not be added.\n ", tunable=Commodity.TunableReference(pack_safe=True)), 'ui_commodity_sort_override': OptionalTunable( description= '\n Optional list of commodities to override the default UI sort order.\n ', tunable=TunableList( description= '\n The position of the commodity in this list represents the sort order.\n Add all possible combination of traits in the list.\n If we have two traits which have sort override, we will implement\n a priority system to determine which determines which trait sort\n order to use.\n ', tunable=Commodity.TunableReference())), 'ui_category': OptionalTunable( description= '\n If enabled then this trait will be displayed in a specific category\n within the relationship panel if this trait would be displayed\n within that panel.\n ', tunable=TunableEnumEntry( description= '\n The UI trait category that we use to categorize this trait\n within the relationship panel.\n ', tunable_type=TraitUICategory, default=TraitUICategory.PERSONALITY), export_modes=ExportModes.All, enabled_name='ui_trait_category_tag'), 'loot_on_trait_add': OptionalTunable( description= '\n If tuned, this list of loots will be applied when trait is added in game.\n ', tunable=TunableList( description= '\n List of loot to apply on the sim when this trait is added not\n through CAS.\n ', tunable=TunableReference( description= '\n Loot to apply.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), pack_safe=True))), 'npc_leave_lot_interactions': OptionalTunable( description= '\n If enabled, allows tuning a set of Leave Lot and Leave Lot Must Run\n interactions that this trait provides. NPC Sims with this trait will\n use these interactions to leave the lot instead of the defaults.\n ', tunable=TunableTuple( description= '\n Leave Lot Now and Leave Lot Now Must Run interactions.\n ', leave_lot_now_interactions=TunableSet( TunableReference( description= '\n If tuned, the Sim will consider these interaction when trying to run\n any "leave lot" situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=False, pack_safe=True)), leave_lot_now_must_run_interactions=TunableSet( TunableReference( description= '\n If tuned, the Sim will consider these interaction when trying to run\n any "leave lot must run" situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=False, pack_safe=True)))), 'hide_relationships': Tunable( description= '\n If checked, then any relationships with a Sim who has this trait\n will not be displayed in the UI. This is done by keeping the\n relationship from having any tracks to actually track which keeps\n it out of the UI.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.RELATIONSHIP), 'whim_set': OptionalTunable( description= '\n If enabled then this trait will offer a whim set to the Sim when it\n is active.\n ', tunable=TunableReference( description= '\n A whim set that is active when this trait is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.ASPIRATION), class_restrictions=('ObjectivelessWhimSet', ))), 'allow_from_gallery': Tunable( description= '\n If checked, then this trait is allowed to be transferred over from\n Sims downloaded from the gallery.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'remove_on_death': Tunable( description= '\n If checked, when a Sim dies this trait will be removed.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), 'build_buy_purchase_tracking': OptionalTunable( description= '\n If enabled, allows this trait to track various build-buy purchases\n via event listening in the trait tracker.\n ', tunable=TunableList( description= '\n Loots to apply to the hamper when clothing pile is being put.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True))) } _asm_param_name = None default_trait_params = set() def __repr__(self): return '<Trait:({})>'.format(self.__name__) def __str__(self): return '{}'.format(self.__name__) @classmethod def _tuning_loaded_callback(cls): cls._asm_param_name = cls.trait_asm_overrides.trait_asm_param if cls._asm_param_name is None: cls._asm_param_name = cls.__name__ if cls.trait_asm_overrides.trait_asm_param is not None and cls.trait_asm_overrides.consider_for_boundary_conditions: cls.default_trait_params.add( cls.trait_asm_overrides.trait_asm_param) for (buff, replacement_buff) in cls.buff_replacements.items(): if buff.trait_replacement_buffs is None: buff.trait_replacement_buffs = {} buff.trait_replacement_buffs[cls] = replacement_buff for mood in cls.excluded_mood_types: if mood.excluding_traits is None: mood.excluding_traits = [] mood.excluding_traits.append(cls) @classmethod def _verify_tuning_callback(cls): if cls.display_name: if not cls.display_name_gender_neutral.hash: logger.error( 'Trait {} specifies a display name. It must also specify a gender-neutral display name. These must use different string keys.', cls, owner='BadTuning') if cls.display_name._string_id == cls.display_name_gender_neutral.hash: logger.error( 'Trait {} has the same string tuned for its display name and its gender-neutral display name. These must be different strings for localization.', cls, owner='BadTuning') if cls.day_night_tracking is not None: if not cls.day_night_tracking.sunlight_buffs and not ( not cls.day_night_tracking.shade_buffs and not (not cls.day_night_tracking.day_buffs and not cls.day_night_tracking.night_buffs)): logger.error( 'Trait {} has Day Night Tracking enabled but no buffs are tuned. Either tune buffs or disable the tracking.', cls, owner='BadTuning') else: tracking_buff_tag = Trait.DAY_NIGHT_TRACKING_BUFF_TAG if any( buff for buff in cls.day_night_tracking.sunlight_buffs if not buff.buff_type.has_tag(tracking_buff_tag) ) or (any(buff for buff in cls.day_night_tracking.shade_buffs if not buff.buff_type.has_tag(tracking_buff_tag)) or any(buff for buff in cls.day_night_tracking.day_buffs if not buff.buff_type.has_tag(tracking_buff_tag)) ) or any( buff for buff in cls.day_night_tracking.night_buffs if not buff.buff_type.has_tag(tracking_buff_tag)): logger.error( 'Trait {} has Day Night tracking with an invalid\n buff. All buffs must be tagged with {} in order to be\n used as part of Day Night Tracking. Add these buffs with the\n understanding that, regardless of what system added them, they\n will always be on the Sim when the condition is met (i.e.\n Sunlight Buffs always added with sunlight is out) and they will\n always be removed when the condition is not met. Even if another\n system adds the buff, they will be removed if this trait is\n tuned to do that.\n ', cls, tracking_buff_tag) for buff_reference in cls.buffs: if buff_reference.buff_type.broadcaster is not None: logger.error( 'Trait {} has a buff {} with a broadcaster tuned that will never be removed. This is a potential performance hit, and a GPE should decide whether this is the best place for such.', cls, buff_reference, owner='rmccord') for commodity in cls.initial_commodities: if not commodity.persisted_tuning: logger.error( 'Trait {} has an initial commodity {} that does not have persisted tuning.', cls, commodity) @classproperty def is_personality_trait(cls): return cls.trait_type == TraitType.PERSONALITY @classproperty def is_aspiration_trait(cls): return cls.trait_type == TraitType.ASPIRATION @classproperty def is_gender_option_trait(cls): return cls.trait_type == TraitType.GENDER_OPTIONS @classproperty def is_ghost_trait(cls): return cls.trait_type == TraitType.GHOST @classproperty def is_robot_trait(cls): return cls.trait_type == TraitType.ROBOT @classmethod def is_valid_trait(cls, sim_info_data): if cls.ages and sim_info_data.age not in cls.ages: return False if cls.genders and sim_info_data.gender not in cls.genders: return False elif cls.species and sim_info_data.species not in cls.species: return False return True @classmethod def should_apply_fixup_actions(cls, fixup_source): if cls.sim_info_fixup_actions and cls.sim_info_fixup_actions_timing == fixup_source: if fixup_source != SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD: logger.warn( 'Trait {} has fixup actions not from CAS flow.This should only happen to old saves before EP08', cls, owner='yozhang') return True return False @classmethod def apply_fixup_actions(cls, sim_info): for fixup_action in cls.sim_info_fixup_actions: fixup_action(sim_info) @classmethod def can_age_up(cls, current_age): if not cls.disable_aging: return True return current_age in cls.disable_aging.allowed_ages @classmethod def is_conflicting(cls, trait): if trait is None: return False if cls.conflicting_traits and trait in cls.conflicting_traits: return True elif trait.conflicting_traits and cls in trait.conflicting_traits: return True return False @classmethod def get_outfit_change_reason(cls, outfit_change_reason): replaced_reason = cls.outfit_replacements.get( outfit_change_reason if outfit_change_reason is not None else OutfitChangeReason.Invalid) if replaced_reason is not None: return replaced_reason elif outfit_change_reason is not None: replaced_reason = cls.outfit_replacements.get( OutfitChangeReason.Invalid) if replaced_reason is not None: return replaced_reason return outfit_change_reason @classmethod def get_teleport_style_interaction_to_inject(cls): return cls.teleport_style_interaction_to_inject
def __init__(self, **kwargs): super().__init__(age_up_warning_notification=UiDialogNotification.TunableFactory(description='\n Message sent to client to warn of impending age up.\n '), age_up_available_notification=UiDialogNotification.TunableFactory(description='\n Message sent to client to alert age up is ready.\n '), age_transition_threshold=Tunable(description='\n Number of Sim days required to be eligible to transition from\n the mapping key age to the next one."\n ', tunable_type=float, default=1), age_transition_warning=Tunable(description='\n Number of Sim days prior to the transition a Sim will get a\n warning of impending new age.\n ', tunable_type=float, default=1), age_transition_delay=Tunable(description='\n Number of Sim days after transition time elapsed before auto-\n aging occurs.\n ', tunable_type=float, default=1), auto_aging_actions=TunableTuple(buff=TunableReference(description='\n Buff that will be applied to the Sim when aging up to the\n current age. This buff will be applied if the Sim\n auto-ages rather than if they age up with the birthday\n cake.\n ', manager=services.buff_manager()), buff_tests=TunableTestSet(description='\n Tests that will be run to determine if to apply the buff on\n the Sim. This should be used to not apply the auto-aging\n under certain circumstances, example auto-aging while a\n birthday party is being thrown for the sim.\n '), description='\n Tuning related to actions that will be applied on auto-aging\n rather than aging up normally through the birday cake.\n '), age_trait_awards=TunableList(description='\n Traits available for selection to the Sim upon completing the\n current age. They traits are presented in a menu for the player\n to choose from.\n ', tunable=TunableReference(services.trait_manager())), age_trait=TunableReference(description="\n The age trait that corresponds to this Sim's age\n ", manager=services.trait_manager()), age_transition_dialog=SimPersonalityAssignmentDialog.TunableFactory(description='\n Dialog displayed to the player when their sim ages up.\n ', locked_args={'phone_ring_type': PhoneRingType.NO_RING}))
class DeathTracker(SimInfoTracker): DEATH_ZONE_ID = 0 DEATH_TYPE_GHOST_TRAIT_MAP = TunableMapping( description= '\n The ghost trait to be applied to a Sim when they die with a given death\n type.\n ', key_type=TunableEnumEntry( description= '\n The death type to map to a ghost trait.\n ', tunable_type=DeathType, default=DeathType.NONE), key_name='Death Type', value_type=TunableReference( description= '\n The ghost trait to apply to a Sim when they die from the specified\n death type.\n ', manager=services.trait_manager()), value_name='Ghost Trait') DEATH_BUFFS = TunableList( description= '\n A list of buffs to apply to Sims when another Sim dies. For example, use\n this tuning to tune a "Death of a Good Friend" buff.\n ', tunable=TunableTuple( test_set=TunableReference( description= "\n The test that must pass between the dying Sim (TargetSim) and\n the Sim we're considering (Actor). If this test passes, no\n further test is executed.\n ", manager=services.get_instance_manager(sims4.resources.Types. SNIPPET), class_restrictions=('TestSetInstance', ), pack_safe=True), buff=TunableBuffReference( description= '\n The buff to apply to the Sim.\n ', pack_safe=True), notification=OptionalTunable( description= '\n If enabled, an off-lot death generates a notification for the\n target Sim. This is limited to one per death instance.\n ', tunable=TunableUiDialogNotificationReference( description= '\n The notification to show.\n ', pack_safe=True)))) IS_DYING_BUFF = TunableReference( description= '\n A reference to the buff a Sim is given when they are dying.\n ', manager=services.buff_manager()) DEATH_RELATIONSHIP_BIT_FIXUP_LOOT = TunableReference( description= '\n A reference to the loot to apply to a Sim upon death.\n \n This is where the relationship bit fixup loots will be tuned. This\n used to be on the interactions themselves but if the interaction was\n reset then the bits would stay as they were. If we add more relationship\n bits we want to clean up on death, the references Loot is the place to \n do it.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION)) def __init__(self, sim_info): self._sim_info = sim_info self._death_type = None self._death_time = None @property def death_type(self): return self._death_type @property def death_time(self): return self._death_time @property def is_ghost(self): return self._sim_info.trait_tracker.has_any_trait( self.DEATH_TYPE_GHOST_TRAIT_MAP.values()) def get_ghost_trait(self): return self.DEATH_TYPE_GHOST_TRAIT_MAP.get(self._death_type) def set_death_type(self, death_type, is_off_lot_death=False): is_npc = self._sim_info.is_npc household = self._sim_info.household self._sim_info.inject_into_inactive_zone(self.DEATH_ZONE_ID, start_away_actions=False, skip_instanced_check=True, skip_daycare=True) household.remove_sim_info(self._sim_info, destroy_if_empty_household=True) if is_off_lot_death: household.pending_urnstone_ids.append(self._sim_info.sim_id) self._sim_info.transfer_to_hidden_household() clubs.on_sim_killed_or_culled(self._sim_info) if death_type is None: return relationship_service = services.relationship_service() for target_sim_info in relationship_service.get_target_sim_infos( self._sim_info.sim_id): resolver = DoubleSimResolver(target_sim_info, self._sim_info) for death_data in self.DEATH_BUFFS: if not death_data.test_set(resolver): continue target_sim_info.add_buff_from_op( death_data.buff.buff_type, buff_reason=death_data.buff.buff_reason) if is_npc and not target_sim_info.is_npc: notification = death_data.notification(target_sim_info, resolver=resolver) notification.show_dialog() break ghost_trait = DeathTracker.DEATH_TYPE_GHOST_TRAIT_MAP.get(death_type) if ghost_trait is not None: self._sim_info.add_trait(ghost_trait) traits = list(self._sim_info.trait_tracker.equipped_traits) for trait in traits: if trait.remove_on_death: self._sim_info.remove_trait(trait) self._death_type = death_type self._death_time = services.time_service().sim_now.absolute_ticks() self._sim_info.reset_age_progress() self._sim_info.resend_death_type() self._handle_remove_rel_bits_on_death() services.get_event_manager().process_event( test_events.TestEvent.SimDeathTypeSet, sim_info=self._sim_info) def _handle_remove_rel_bits_on_death(self): resolver = SingleSimResolver(self._sim_info) if self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT is not None: for (loot, _) in self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT.get_loot_ops_gen( ): result = loot.test_resolver(resolver) if result: loot.apply_to_resolver(resolver) def clear_death_type(self): self._death_type = None self._death_time = None self._sim_info.resend_death_type() def save(self): if self._death_type is not None: data = protocols.PersistableDeathTracker() data.death_type = self._death_type data.death_time = self._death_time return data def load(self, data): try: self._death_type = DeathType(data.death_type) except: self._death_type = DeathType.NONE self._death_time = data.death_time @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.MINIMUM
class Fish(objects.game_object.GameObject): __qualname__ = 'Fish' INSTANCE_TUNABLES = { 'fishbowl_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use when this fish is dropped in a fish bowl.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fishing_hole_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use at the fishing hole (pond) where this\n fish can be caught.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fishing_spot_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use at the fishing spot (sign) where this\n fish can be caught.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'wall_mounted_object': sims4.tuning.tunable.TunableReference( description= '\n When this fish is mounted to the wall, this is the object it will turn in to.\n ', manager=services.definition_manager(), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fish_weight': sims4.tuning.tunable.TunableInterval( description= '\n The weight range of this fish. Each fish caught will get a random\n weight that lies between these numbers, inclusively.\n ', tunable_type=float, default_lower=0.0, default_upper=1.0, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'catchable_tests': event_testing.tests.TunableTestSet( description= "\n If these tests pass, the Sim can catch this fish.\n If these tests fail, the Sim can not catch this fish.\n This doesn't stop the Sim from trying to catch these fish, but it\n will never happen.\n \n DO NOT add bait buffs here. Those should be added to the Required Bait tunable field.\n \n When testing on fishing skill be sure to enable 'Use Effective\n Skill Level' since baits can change it.\n ", tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'required_bait_buff': sims4.tuning.tunable.OptionalTunable( description= '\n The bait buff that is required to catch this fish.\n \n If this is tuned, this fish can not be caught without the required bait.\n If this is not tuned, this fish can be caught with or without bait.\n \n Note: Bait buffs are the only buffs that should be tuned here.\n If you want to gate this fish on a non-bait buff, use the Catchable Tests.\n ', tunable=sims4.tuning.tunable.TunableReference( manager=services.buff_manager()), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fish_type': sims4.tuning.tunable.Tunable( description= "\n The asm parameter for the size of the fish. If you're unsure what\n this should be set to, talk to the animator or modeler and ask what\n fish type this fish should be.\n ", tunable_type=str, default=None, source_query=sims4.tuning.tunable_base.SourceQueries. SwingEnumNamePattern.format('fishType'), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'skill_weight_curve': sims4.tuning.geometric.TunableCurve( description= "\n This curve represents the mean weight in kg of the fish based on the Sims's fishing skill level.\n The X axis is the Sim's effective fishing skill level.\n The Y axis is the mean weight, in kg, of the fish.\n The mean weight will be modified by the Mean Weight Deviation field.\n ", x_axis_name='Effective Fishing Skill Level', y_axis_name='Mean Weight (kg)', tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'mean_weight_deviation': sims4.tuning.tunable.Tunable( description= '\n This is the amount of deviation from the mean the weight can be.\n The mean weight is first decided then multiplied by this number.\n The result is both added and subtracted from the mean weight to get\n the min/max possible weight of the fish. We then pick a random\n number between the min and max to determine the final weight of the\n fish.\n \n Example: Assume Mean Weight = 2 and Mean Weight Deviation = 0.2\n 2 x 0.2 = 0.4\n min = 2 - 0.4 = 1.6\n max = 2 + 0.4 = 2.4\n A random number is chosen between 1.6 and 2.4, inclusively.\n ', tunable_type=float, default=1, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'weight_money_multiplier': sims4.tuning.tunable.Tunable( description= '\n The weight of the fish will be multiplied by this number then the\n result of that multiplication will be added to the base value of\n the fish.\n ', tunable_type=float, default=1, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'buffs_on_catch': sims4.tuning.tunable.TunableList( description= '\n A list of buffs to award the Sim when they catch this fish.\n ', tunable=buffs.tunable.TunableBuffReference(), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING) } FISHING_SKILL_STATISTIC = sims4.tuning.tunable.TunableReference( description= '\n The fishing skill stat. This just makes lookups on the fishing skill easier.\n ', manager=services.statistic_manager()) FISH_FRESHNESS_STATE = objects.components.state.ObjectState.TunableReference( description='\n The statistic used for fish freshness.\n ' ) WEIGHT_STATISTIC = sims4.tuning.tunable.TunableReference( description= '\n The weight statistic that will be added to the fish and set as they\n are caught.\n ', manager=services.statistic_manager()) LOCALIZED_WEIGHT = sims4.localization.TunableLocalizedStringFactory( description= "\n How the weight should appear when used in other strings, like the\n 'catch fish' notification. i.e. '2.2 kg'\n {0.Number} = weight value\n " ) MINIMUM_FISH_WEIGHT = 0.1 @sims4.utils.flexmethod def can_catch(cls, inst, resolver, require_bait=False): inst_or_cls = inst if inst is not None else cls if require_bait: sim = resolver.get_participant(interactions.ParticipantType.Actor) if inst_or_cls.required_bait_buff and not sim.has_buff( inst_or_cls.required_bait_buff): return False return inst_or_cls.catchable_tests.run_tests(resolver) def on_add(self): super().on_add() self.add_state_changed_callback(self._on_state_or_name_changed) self.add_name_changed_callback(self._on_state_or_name_changed) def get_object_property(self, property_type): if property_type == objects.game_object_properties.GameObjectProperty.FISH_FRESHNESS: return self.get_state(self.FISH_FRESHNESS_STATE).display_name return super().get_object_property(property_type) def initialize_fish(self, sim): fishing_stat = sim.get_statistic(self.FISHING_SKILL_STATISTIC) skill_level = 1 if fishing_stat is None else sim.get_effective_skill_level( fishing_stat) mean_weight = self.skill_weight_curve.get(skill_level) deviation = mean_weight * self.mean_weight_deviation weight_min = max(mean_weight - deviation, self.MINIMUM_FISH_WEIGHT) weight_max = mean_weight + deviation actual_weight = sims4.random.uniform(weight_min, weight_max) fish_stat_tracker = self.get_tracker(self.WEIGHT_STATISTIC) fish_stat_tracker.set_value(self.WEIGHT_STATISTIC, actual_weight) self.update_ownership(sim) self.update_object_tooltip() def get_catch_buffs_gen(self): yield self.buffs_on_catch def get_localized_weight(self): stat_tracker = self.get_tracker(self.WEIGHT_STATISTIC) return self.LOCALIZED_WEIGHT( stat_tracker.get_user_value(self.WEIGHT_STATISTIC)) def _on_state_or_name_changed(self, *_, **__): fishbowl = self._try_get_fishbowl() if fishbowl is not None: fishbowl.update_object_tooltip() def on_remove(self): self.remove_state_changed_callback(self._on_state_or_name_changed) self.remove_name_changed_callback(self._on_state_or_name_changed) super().on_remove() def _ui_metadata_gen(self): tooltip_component = self.get_component( objects.components.types.TOOLTIP_COMPONENT) yield tooltip_component._ui_metadata_gen() def _try_get_fishbowl(self): inventory_owner = self.inventoryitem_component._last_inventory_owner if isinstance(inventory_owner, fishing.fish_bowl_object.FishBowl): return inventory_owner
class BuffRemovalOp(BaseLootOperation): FACTORY_TUNABLES = { 'remove_all_visible_buffs': Tunable( description= "\n If checked, all visible buffs on the Sim, excluding those specified in\n the 'buffs_to_ignore' list will be removed. If unchecked, buff removal\n will be handled by the 'buffs_to_remove' list.\n ", tunable_type=bool, default=False), 'buffs_to_remove': TunableList( description= "\n If 'remove_all_buffs' is not checked, this is the list of buffs that\n will be removed from the subject. If 'remove_all_buffs' is checked,\n this list will be ignored.\n ", tunable=TunableReference( description= '\n Buff to be removed.\n ', manager=services.buff_manager(), pack_safe=True)), 'buff_tags_to_remove': TunableTags( description= "\n If 'remove_all_buffs' is not checked, buffs with any tag in this list\n will be removed from the subject. If 'remove_all_buffs' is checked, this\n list will be ignored. You can also specify how many buffs you want to remove\n by tags in count_to_remove_by_tags\n ", filter_prefixes=('buff', )), 'count_to_remove_by_tags': OptionalTunable(tunable=TunableRange( description= '\n If enabled, randomly remove x number of buffs specified in buff_tags_to_remove.\n If disabled, all buffs specified in buff_tags_to_remove will be removed\n ', tunable_type=int, default=1, minimum=1)), 'buffs_to_ignore': TunableList( description= "\n If 'remove_all_buffs' is checked, no buffs included in this list will\n be removed. If 'remove_all_buffs' is unchecked, this list will be\n ignored.\n ", tunable=TunableReference( description= '\n Buff to be removed.\n ', manager=services.buff_manager())) } def __init__(self, remove_all_visible_buffs, buffs_to_remove, buff_tags_to_remove, count_to_remove_by_tags, buffs_to_ignore, **kwargs): super().__init__(**kwargs) self._remove_all_visible_buffs = remove_all_visible_buffs self._buffs_to_remove = buffs_to_remove self._buff_tags_to_remove = buff_tags_to_remove self._count_to_remove_by_tags = count_to_remove_by_tags self._buffs_to_ignore = buffs_to_ignore def _apply_to_subject_and_target(self, subject, target, resolver): if self._remove_all_visible_buffs: removal_list = [] removal_list.extend(subject.Buffs) for buff in removal_list: if type(buff) in self._buffs_to_ignore: continue if not buff.visible: continue if buff.commodity is not None: if subject.is_statistic_type_added_by_modifier( buff.commodity): continue tracker = subject.get_tracker(buff.commodity) commodity_inst = tracker.get_statistic(buff.commodity) if commodity_inst is not None and commodity_inst.core: continue subject.Buffs.remove_buff_entry(buff) else: subject.Buffs.remove_buffs_by_tags( self._buff_tags_to_remove, count_to_remove=self._count_to_remove_by_tags) else: for buff_type in self._buffs_to_remove: subject.Buffs.remove_buff_by_type(buff_type) subject.Buffs.remove_buffs_by_tags( self._buff_tags_to_remove, count_to_remove=self._count_to_remove_by_tags)
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 Fish(objects.game_object.GameObject): INSTANCE_TUNABLES = { 'fishbowl_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use when this fish is dropped in a fish bowl.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'inventory_to_fish_vfx': sims4.tuning.tunable.TunableMapping( description= '\n The inventory type to fish vfx to play when fish is placed in\n inventory type. If inventory type does not exist in mapping, use\n fishbowl_vfx as fallback vfx to play.\n ', key_type=sims4.tuning.tunable.TunableEnumEntry( tunable_type=objects.components.inventory_enums.InventoryType, default=objects.components.inventory_enums.InventoryType. UNDEFINED), value_type=sims4.tuning.tunable.TunableTuple( vfx_name=sims4.tuning.tunable.Tunable(tunable_type=str, default=''), vfx_base_bone_name=sims4.tuning.tunable.Tunable( tunable_type=str, default='_FX_fish_')), key_name='inventory_type', value_name='base vfx name', tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fishing_hole_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use at the fishing hole (pond) where this\n fish can be caught.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fishing_spot_vfx': sims4.tuning.tunable.Tunable( description= '\n The name of the VFX to use at the fishing spot (sign) where this\n fish can be caught.\n ', tunable_type=str, default=None, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'wall_mounted_object': sims4.tuning.tunable.TunableReference( description= '\n When this fish is mounted to the wall, this is the object it will turn in to.\n ', manager=services.definition_manager(), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'catchable_tests': event_testing.tests.TunableTestSet( description= "\n If these tests pass, the Sim can catch this fish.\n If these tests fail, the Sim can not catch this fish.\n This doesn't stop the Sim from trying to catch these fish, but it\n will never happen.\n \n DO NOT add bait buffs here. Those should be added to the Required Bait tunable field.\n \n When testing on fishing skill be sure to enable 'Use Effective\n Skill Level' since baits can change it.\n ", tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'required_bait_buff': sims4.tuning.tunable.OptionalTunable( description= '\n The bait buff that is required to catch this fish.\n \n If this is tuned, this fish can not be caught without the required bait.\n If this is not tuned, this fish can be caught with or without bait.\n \n Note: Bait buffs are the only buffs that should be tuned here.\n If you want to gate this fish on a non-bait buff, use the Catchable Tests.\n ', tunable=sims4.tuning.tunable.TunableReference( manager=services.buff_manager()), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'fish_type': sims4.tuning.tunable.Tunable( description= "\n The asm parameter for the size of the fish. If you're unsure what\n this should be set to, talk to the animator or modeler and ask what\n fish type this fish should be.\n ", tunable_type=str, default=None, source_query=sims4.tuning.tunable_base.SourceQueries. SwingEnumNamePattern.format('fishType'), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'skill_weight_curve': sims4.tuning.geometric.TunableCurve( description= "\n This curve represents the mean weight in kg of the fish based on the Sims's fishing skill level.\n The X axis is the Sim's effective fishing skill level.\n The Y axis is the mean weight, in kg, of the fish.\n The mean weight will be modified by the Mean Weight Deviation field.\n ", x_axis_name='Effective Fishing Skill Level', y_axis_name='Mean Weight (kg)', tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'mean_weight_deviation': sims4.tuning.tunable.Tunable( description= '\n This is the amount of deviation from the mean the weight can be.\n The mean weight is first decided then multiplied by this number.\n The result is both added and subtracted from the mean weight to get\n the min/max possible weight of the fish. We then pick a random\n number between the min and max to determine the final weight of the\n fish.\n \n Example: Assume Mean Weight = 2 and Mean Weight Deviation = 0.2\n 2 x 0.2 = 0.4\n min = 2 - 0.4 = 1.6\n max = 2 + 0.4 = 2.4\n A random number is chosen between 1.6 and 2.4, inclusively.\n ', tunable_type=float, default=1, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'weight_money_multiplier': sims4.tuning.tunable.Tunable( description= '\n The weight of the fish will be multiplied by this number then the\n result of that multiplication will be added to the base value of\n the fish.\n ', tunable_type=float, default=1, tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'global_policy_value_mapping': TunableMapping( description= '\n The mapping of global policies that when enacted are used to\n increment the base value of the fish by a percent of its original value.\n ', key_type=TunableReference( description= '\n The global policy that when completed updates the cost of the\n fish by the paired percent.\n ', manager=services.get_instance_manager( sims4.resources.Types.SNIPPET), class_restrictions=('GlobalPolicy', ), pack_safe=True), value_type=TunablePercent( description= "\n The percent of the fish's value to increment the base value.\n ", default=50), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING), 'buffs_on_catch': sims4.tuning.tunable.TunableList( description= '\n A list of buffs to award the Sim when they catch this fish.\n ', tunable=buffs.tunable.TunableBuffReference(), tuning_group=sims4.tuning.tunable_base.GroupNames.FISHING) } FISHING_SKILL_STATISTIC = sims4.tuning.tunable.TunableReference( description= '\n The fishing skill stat. This just makes lookups on the fishing skill easier.\n ', manager=services.statistic_manager()) FISH_FRESHNESS_STATE = objects.components.state.ObjectState.TunableReference( description='\n The statistic used for fish freshness.\n ' ) WEIGHT_STATISTIC = sims4.tuning.tunable.TunableReference( description= '\n The weight statistic that will be added to the fish and set as they\n are caught.\n ', manager=services.statistic_manager()) LOCALIZED_WEIGHT = sims4.localization.TunableLocalizedStringFactory( description= "\n How the weight should appear when used in other strings, like the\n 'catch fish' notification. i.e. '2.2 kg'\n {0.Number} = weight value\n " ) MINIMUM_FISH_WEIGHT = 0.1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._active_global_policy_modifiers = None @sims4.utils.flexmethod def can_catch(cls, inst, resolver, require_bait=False): inst_or_cls = inst if inst is not None else cls if require_bait: sim = resolver.get_participant(interactions.ParticipantType.Actor) if inst_or_cls.required_bait_buff and not sim.has_buff( inst_or_cls.required_bait_buff): return False return inst_or_cls.catchable_tests.run_tests(resolver) def on_add(self): super().on_add() self.add_state_changed_callback(self._on_state_or_name_changed) self.add_name_changed_callback(self._on_state_or_name_changed) self._update_fish_cost(self.WEIGHT_STATISTIC.default_value) self._register_for_tuned_global_policy_events() def _update_fish_cost(self, new_cost): fish_stat_tracker = self.get_tracker(self.WEIGHT_STATISTIC) fish_stat_tracker.set_value(self.WEIGHT_STATISTIC, new_cost) self.base_value += int(new_cost * self.weight_money_multiplier) self.remove_global_policy_value_mod() self.add_global_policy_value_mod() self.update_object_tooltip() def _register_for_tuned_global_policy_events(self): active_global_policies = self._active_global_policy_modifiers is not None for policy in self.global_policy_value_mapping: if active_global_policies and policy in self._active_global_policy_modifiers: continue services.get_event_manager().register_with_custom_key( self, TestEvent.GlobalPolicyProgress, policy) def remove_global_policy_value_mod(self): if self._active_global_policy_modifiers is None: return global_policy_service = services.global_policy_service() if global_policy_service is None: return total_percent_decrease = 1.0 policies_to_remove = [] enacted_policies = global_policy_service.get_enacted_global_policies() for modifying_policy in self._active_global_policy_modifiers: if modifying_policy not in enacted_policies: total_percent_decrease += self.global_policy_value_mapping.get( type(modifying_policy)) services.get_event_manager().register_with_custom_key( self, TestEvent.GlobalPolicyProgress, type(modifying_policy)) policies_to_remove.append(modifying_policy) for policy_to_remove in policies_to_remove: self._active_global_policy_modifiers.remove(policy_to_remove) if total_percent_decrease != 0: self.base_value = int(self.base_value / total_percent_decrease) def add_global_policy_value_mod(self): if not self.global_policy_value_mapping: return global_policy_service = services.global_policy_service() if global_policy_service is None: return total_percent_increase = 0 active_global_policy_modifiers = self._active_global_policy_modifiers is not None for enacted_policy in global_policy_service.get_enacted_global_policies( ): if active_global_policy_modifiers and enacted_policy in self._active_global_policy_modifiers: continue policy_percent_increase = self.global_policy_value_mapping.get( type(enacted_policy)) if policy_percent_increase: if not active_global_policy_modifiers: self._active_global_policy_modifiers = [enacted_policy] else: self._active_global_policy_modifiers.append(enacted_policy) services.get_event_manager().register_with_custom_key( self, TestEvent.GlobalPolicyProgress, type(enacted_policy)) total_percent_increase += policy_percent_increase self.base_value += int(self.base_value * total_percent_increase) def get_object_property(self, property_type): if property_type == objects.game_object_properties.GameObjectProperty.FISH_FRESHNESS: return self.get_state(self.FISH_FRESHNESS_STATE).display_name else: return super().get_object_property(property_type) def initialize_fish(self, sim): fishing_stat = sim.get_statistic(self.FISHING_SKILL_STATISTIC) skill_level = 1 if fishing_stat is None else sim.get_effective_skill_level( fishing_stat) mean_weight = self.skill_weight_curve.get(skill_level) deviation = mean_weight * self.mean_weight_deviation weight_min = max(mean_weight - deviation, self.MINIMUM_FISH_WEIGHT) weight_max = mean_weight + deviation actual_weight = sims4.random.uniform(weight_min, weight_max) self._update_fish_cost(actual_weight) self.update_ownership(sim) def get_catch_buffs_gen(self): yield from self.buffs_on_catch def get_localized_weight(self): stat_tracker = self.get_tracker(self.WEIGHT_STATISTIC) return self.LOCALIZED_WEIGHT( stat_tracker.get_user_value(self.WEIGHT_STATISTIC)) def get_notebook_information(self, notebook_entry, notebook_sub_entries): sub_entries = None if notebook_sub_entries is not None: for sub_entry in notebook_sub_entries: bait_data = FishingTuning.get_fishing_bait_data( sub_entry.definition) if bait_data is not None: if sub_entries is None: sub_entries = [] sub_entries.append(SubEntryData(bait_data.guid64, True)) return (notebook_entry(self.definition.id, sub_entries=sub_entries), ) def _on_state_or_name_changed(self, *_, **__): fishbowl = self._try_get_fishbowl() if fishbowl is not None: fishbowl.update_object_tooltip() def handle_event(self, sim_info, event_type, resolver): if event_type == TestEvent.GlobalPolicyProgress: self._update_fish_cost(self.WEIGHT_STATISTIC.default_value) def on_remove(self): self.remove_state_changed_callback(self._on_state_or_name_changed) self.remove_name_changed_callback(self._on_state_or_name_changed) if self.global_policy_value_mapping: for policy in self.global_policy_value_mapping: services.get_event_manager().unregister_with_custom_key( self, TestEvent.GlobalPolicyProgress, policy) if self._active_global_policy_modifiers is not None: for modifying_policy in self._active_global_policy_modifiers: services.get_event_manager().unregister_with_custom_key( self, TestEvent.GlobalPolicyProgress, type(modifying_policy)) self._active_global_policy_modifiers = None super().on_remove() def _ui_metadata_gen(self): tooltip_component = self.get_component( objects.components.types.TOOLTIP_COMPONENT) yield from tooltip_component._ui_metadata_gen() def _try_get_fishbowl(self): inventory_owner = self.inventoryitem_component.last_inventory_owner if isinstance(inventory_owner, fishing.fish_bowl_object.FishBowl): return inventory_owner
class FishingLocationGoFishingSuperInteraction(FishingLocationSuperInteraction ): __qualname__ = 'FishingLocationGoFishingSuperInteraction' BAIT_TAG_BUFF_MAP = sims4.tuning.tunable.TunableMapping( key_type=sims4.tuning.tunable.TunableEnumEntry( description= '\n The bait tag to which we want to map a buff.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID), key_name='Bait Tag', value_type=sims4.tuning.tunable.TunableReference( manager=services.buff_manager()), value_name='Bait Buff') FISHING_WITH_BAIT_INTERACTION_NAME = sims4.localization.TunableLocalizedStringFactory( description= '\n When a Sim fishes with bait, this is the interaction name. This name\n will revert to the normal name of the interaction when they run out of\n bait.\n \n Uses the same tokens as the interaction display name.\n ' ) OUT_OF_BAIT_NOTIFICATION = ui.ui_dialog_notification.UiDialogNotification.TunableFactory( description= "\n This notification will be displayed when the player started using bait but ran out.\n Token 0 is the actor sim. e.g. {0.SimFirstName}\n Token 1 is the target fishing location (probably don't want to use this.\n Token 2 is the bait object they just ran out of. e.g. {2.ObjectCatalogName} will show the type\n " ) def __init__(self, aop, context, *args, exit_functions=(), force_inertial=False, additional_post_run_autonomy_commodities=None, **kwargs): super().__init__(aop, context, exit_functions=(), force_inertial=False, additional_post_run_autonomy_commodities=None, *args, **kwargs) self._bait = None self._buff_handle_ids = [] def build_basic_elements(self, sequence=(), **kwargs): sequence = super().build_basic_elements(sequence=sequence, **kwargs) sequence = element_utils.build_critical_section_with_finally( self._interaction_start, sequence, self._interaction_end) return sequence @property def bait(self): return self._bait def _interaction_start(self, _): self._bait = self.get_participant( interactions.ParticipantType.PickedObject) self._try_apply_bait_and_buffs() def _try_apply_bait_and_buffs(self): if self._bait: if not self.sim.inventory_component.try_move_object_to_hidden_inventory( self._bait): logger.error('Tried hiding the bait object, {}, but failed.', self._bait) self._bait = None else: self._add_bait_buffs() self.sim.ui_manager.set_interaction_icon_and_name( self.id, icon=None, name=self.create_localized_string( localized_string_factory=self. FISHING_WITH_BAIT_INTERACTION_NAME)) def _interaction_end(self, _): if self._bait: sim_inventory = self.sim.inventory_component if not sim_inventory.try_remove_object_by_id(self._bait.id): logger.error( "Tried removing the bait object, {}, but it couldn't be found.", self._bait) if not sim_inventory.player_try_add_object(self._bait): logger.error( "Tried adding the bait object, {}, back into the sim's, {}, inventory but failed.", self._bait, self.sim) self._remove_bait_buffs() def kill_and_try_reapply_bait(self): if self._bait: sim_inventory = self.sim.inventory_component old_bait = self._bait self._bait = sim_inventory.get_item_with_definition( old_bait.definition, ignore_hidden=True) if self._bait is not None: self._try_apply_bait_and_buffs() else: self._remove_bait_buffs() self.sim.ui_manager.set_interaction_icon_and_name( self.id, icon=None, name=self.get_name()) notification = self.OUT_OF_BAIT_NOTIFICATION( self.sim, self.get_resolver()) notification.show_dialog(additional_tokens=(old_bait, )) if not sim_inventory.try_remove_object_by_id(old_bait.id): logger.error( "Tried destroying the bait object, {}, but the destroy failed. It probably wasn't found in the sim's inventory or hidden inventory.", old_bait) def _add_bait_buffs(self): if self._bait: for (tag, buff) in self.BAIT_TAG_BUFF_MAP.items(): while self._bait.has_tag(tag): self._buff_handle_ids.append(self.sim.add_buff(buff)) def _remove_bait_buffs(self): for handle_id in self._buff_handle_ids: self.sim.remove_buff(handle_id) self._buff_handle_ids = []
class DynamicBuffLootOp(BaseLootOperation): FACTORY_TUNABLES = { 'description': '\n This loot will give a random buff based on the weight get tuned inside.\n ', 'buffs': TunableMapping( description='\n ', key_type=TunableReference( description= '\n Buff that will get this weight in the random.', manager=services.buff_manager()), value_type=Tunable( description='\n The weight value.', tunable_type=float, default=0)), 'buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )) } def __init__(self, buffs, buff_reason, **kwargs): super().__init__(**kwargs) self._buffs = buffs self._buff_reason = buff_reason self._random_buff = None @TunableFactory.factory_option def subject_participant_type_options(description=singletons.DEFAULT, **kwargs): return BaseLootOperation.get_participant_tunable( *('subject', ), invalid_participants=(ParticipantType.Invalid, ParticipantType.All, ParticipantType.PickedItemId), **kwargs) def _get_random_buff(self): if self._random_buff is None: buff_pair_list = list(self._buffs.items()) self._random_buff = sims4.random.pop_weighted(buff_pair_list, flipped=True) return self._random_buff def _apply_to_subject_and_target(self, subject, target, resolver): random_buff = self._get_random_buff() if random_buff is not None: if not subject.is_sim: logger.error( 'Tuning error: subject {} of DynamicBuffLootOp giving buff {} for reason {} is not a sim', self.subject, random_buff, self._buff_reason) return subject.add_buff_from_op(random_buff, self._buff_reason) def _on_apply_completed(self): random_buff = self._random_buff self._random_buff = None return random_buff