class Mood(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.mood_manager()): __qualname__ = 'Mood' INSTANCE_TUNABLES = {'mood_asm_param': OptionalTunable(description='\n If set, then this mood will specify an asm parameter to affect\n animations. If not set, then the ASM parameter will be determined by\n the second most prevalent mood.\n ', tunable=Tunable(description="\n The asm parameter for Sim's mood, if not set, will use 'xxx'\n from instance name pattern with 'mood_xxx'.\n ", tunable_type=str, default='', source_query=SourceQueries.SwingEnumNamePattern.format('mood')), enabled_name='Specify', disabled_name='Determined_By_Other_Moods'), 'intensity_thresholds': TunableList(int, description='\n List of thresholds at which the intensity of this mood levels up.\n If empty, this mood has a single threshold and all mood tuning lists should\n have a single item in them.\n For each threshold added, you may add a new item to the Buffs, Mood Names,\n Portrait Pose Indexes and Portrait Frames lists.'), 'buffs': TunableList(TunableBuffReference(reload_dependent=True), description='\n A list of buffs that will be added while this mood is the active mood\n on a Sim. \n The first item is applied for the initial intensity, and each\n subsequent item replaces the previous buff as the intensity levels up.'), 'mood_names': TunableList(TunableLocalizedString(), description='\n A list of localized names of this mood.\n The first item is applied for the initial intensity, and each\n subsequent item replaces the name as the intensity levels up.', export_modes=(ExportModes.ServerXML, ExportModes.ClientBinary)), 'portrait_pose_indexes': TunableList(Tunable(tunable_type=int, default=0), description='\n A list of the indexes of the pose passed to thumbnail generation on the\n client to pose the Sim portrait when they have this mood.\n You can find the list of poses in tuning\n (Client_ThumnailPoses)\n The first item is applied for the initial intensity, and each\n subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'portrait_frames': TunableList(Tunable(tunable_type=str, default=''), description='\n A list of the frame labels (NOT numbers!) from the UI .fla file that the\n portrait should be set to when this mood is active. Determines\n background color, font color, etc.\n The first item is applied for the initial intensity, and each\n subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'environment_scoring_commodity': Commodity.TunableReference(description="\n Defines the ranges and corresponding buffs to apply for this\n mood's environmental contribution.\n \n Be sure to tune min, max, and the different states. The\n convergence value is what will remove the buff. Suggested to be\n 0.\n "), 'descriptions': TunableList(TunableLocalizedString(), description='\n Description for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'icons': TunableList(TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE), description='\n Icon for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'descriptions_age_override': TunableMapping(description='\n Mapping of age to descriptions text for mood. If age does not\n exist in mapping will use default description text.\n ', key_type=TunableEnumEntry(sim_info_types.Age, sim_info_types.Age.CHILD), value_type=TunableList(description='\n Description for the UI tooltip, per intensity.\n ', tunable=TunableLocalizedString()), key_name='Age', value_name='description_text', export_modes=(ExportModes.ClientBinary,)), 'descriptions_trait_override': TunableMoodDescriptionTraitOverride(description='\n Trait override for mood descriptions. If a Sim has this trait\n and there is not a valid age override for the Sim, this\n description text will be used.\n ', export_modes=(ExportModes.ClientBinary,)), 'audio_stings_on_add': TunableList(description="\n The audio to play when a mood or it's intensity changes. Tune one for each intensity on the mood.\n ", tunable=TunableResourceKey(description='\n The sound to play.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.ClientBinary)), 'mood_colors': TunableList(description='\n A list of the colors displayed on the steel series mouse when the active Sim has this mood. The first item is applied for the initial intensity, and each subsequent item replaces the color as the intensity levels up.\n ', tunable=TunableVector3(description='\n Color.\n ', default=sims4.math.Vector3.ZERO(), export_modes=ExportModes.ClientBinary)), 'mood_frequencies': TunableList(description='\n A list of the flash frequencies on the steel series mouse when the active Sim has this mood. The first item is applied for the initial intensity, and each subsequent item replaces the value as the intensity levels up. 0 => solid color, otherwise, value => value hertz.\n ', tunable=Tunable(tunable_type=float, default=0.0, description=',\n Hertz.\n ', export_modes=ExportModes.ClientBinary)), 'buff_polarity': TunableEnumEntry(description='\n Setting the polarity will determine how up/down arrows\n appear for any buff that provides this mood.\n ', tunable_type=BuffPolarity, default=BuffPolarity.NEUTRAL, tuning_group=GroupNames.UI, needs_tuning=True, export_modes=ExportModes.All), 'is_changeable': Tunable(description='\n If this is checked, any buff with this mood will change to\n the highest current mood of the same polarity. If there is no mood\n with the same polarity it will default to use this mood type\n ', tunable_type=bool, default=False, needs_tuning=True)} _asm_param_name = None excluding_traits = None @classmethod def _tuning_loaded_callback(cls): cls._asm_param_name = cls.mood_asm_param if not cls._asm_param_name: name_list = cls.__name__.split('_', 1) if len(name_list) <= 1: logger.error("Mood {} has an invalid name for asm parameter, please either set 'mood_asm_param' or change the tuning file name to match the format 'mood_xxx'.", cls.__name__) cls._asm_param_name = name_list[1] cls._asm_param_name = cls._asm_param_name.lower() for buff_ref in cls.buffs: my_buff = buff_ref.buff_type while my_buff is not None: if my_buff.mood_type is not None: logger.error('Mood {} will apply a buff ({}) that affects mood. This can cause mood calculation errors. Please select a different buff or remove the mood change.', cls.__name__, my_buff.mood_type.__name__) my_buff.is_mood_buff = True prev_threshold = 0 for threshold in cls.intensity_thresholds: if threshold <= prev_threshold: logger.error('Mood {} has Intensity Thresholds in non-ascending order.') break prev_threshold = threshold @classmethod def _verify_tuning_callback(cls): num_thresholds = len(cls.intensity_thresholds) + 1 if len(cls.buffs) != num_thresholds: logger.error('Mood {} does not have the correct number of Buffs tuned. It has {} thresholds, but {} buffs.', cls.__name__, num_thresholds, len(cls.buffs)) if len(cls.mood_names) != num_thresholds: logger.error('Mood {} does not have the correct number of Mood Names tuned. It has {} thresholds, but {} names.', cls.__name__, num_thresholds, len(cls.mood_names)) if len(cls.portrait_pose_indexes) != num_thresholds: logger.error('Mood {} does not have the correct number of Portrait Pose Indexes tuned. It has {} thresholds, but {} poses.', cls.__name__, num_thresholds, len(cls.portrait_pose_indexes)) if len(cls.portrait_frames) != num_thresholds: logger.error('Mood {} does not have the correct number of Portrait Frames tuned. It has {} thresholds, but {} frames.', cls.__name__, num_thresholds, len(cls.portrait_frames)) for (age, descriptions) in cls.descriptions_age_override.items(): while len(descriptions) != num_thresholds: logger.error('Mood {} does not have the correct number of descriptions age override tuned. For age:({}) It has {} thresholds, but {} descriptions.', cls.__name__, age, num_thresholds, len(descriptions)) if cls.descriptions_trait_override.trait is not None and len(cls.descriptions_trait_override.descriptions) != num_thresholds: logger.error('Mood {} does not have the correct number of trait override descriptions tuned. For trait:({}) It has {} thresholds, but {} descriptions.', cls.__name__, cls.descriptions_trait_override.trait.__name__, num_thresholds, len(cls.descriptions_trait_override.descriptions)) @classproperty def asm_param_name(cls): return cls._asm_param_name
class MusicTrack(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RECIPE)): INSTANCE_TUNABLES = { 'music_clip': OptionalTunable( description= '\n If enabled, the music clip for music interactions. If disabled,\n make sure you have vocals tuned.\n ', tunable=TunableResourceKey( description= '\n The propx file of the music clip to play.\n ', needs_tuning=False, resource_types=(sims4.resources.Types.PROPX, ))), 'length': TunableRealSecond( description= "\n The length of the clip in real seconds. This should be a part of\n the propx's file name.\n ", needs_tuning=False, default=30, minimum=0), 'buffer': TunableRealSecond( description= "\n A buffer added to the track length. This is used to prevent the\n audio from stopping before it's finished.\n ", needs_tuning=False, default=0), 'check_for_unlock': Tunable( description= "\n Whether or not to check the Sim's Unlock Component to determine if\n they can play the song. Currently, only clips that are meant to be\n unlocked by the Write Song interaction should have this set to true.\n ", needs_tuning=False, tunable_type=bool, default=False), 'music_track_name': OptionalTunable( description= "\n If the clip is of a song, this is its name. The name is shown in the\n Pie Menu when picking specific songs to play.\n \n If the clip isn't a song, like clips used for the Practice or Write\n Song interactions, this does not need to be tuned.\n ", tunable=TunableLocalizedStringFactory( description= "\n The track's name.\n "), enabled_by_default=True), 'tests': TunableTestSet( description= '\n Tests to verify if this song is available for the Sim to play.\n ' ), 'moods': TunableList( description= "\n A list of moods that will be used to determine which song a Sim will\n play autonomously. If a Sim doesn't know any songs that their\n current mood, they'll play anything.\n ", tunable=TunableReference(manager=services.mood_manager()), needs_tuning=True), 'vocals': TunableMapping( description= "\n A mapping of participants and their potential vocal tracks. Each\n participant that has a vocal track that tests successfully will\n sing when the music starts.\n \n Note: The interaction's resolver will be passed into the vocal\n track tests, so use the same participant in those tests.\n ", key_name='participant', value_name='vocal_tracks', key_type=TunableEnumEntry( description= '\n The participant who should sing vocals when the music starts.\n ', tunable_type=ParticipantTypeSim, default=ParticipantTypeSim.Actor), value_type=TunableList( description= '\n If this music track has vocals, add them here. The first track that\n passes its test will be used. If no tracks pass their test, none\n will be used.\n ', tunable=VocalTrack.TunableReference())) } @classmethod def _verify_tuning_callback(cls): if cls.music_clip is None and not cls.vocals: logger.error('{} does not have music or vocals tuned.', cls, owner='rmccord') @classproperty def tuning_tags(cls): return EMPTY_SET
class AwayAction(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.AWAY_ACTION)): __qualname__ = 'AwayAction' INSTANCE_TUNABLES = {'_exit_conditions': TunableList(description='\n A list of exit conditions for this away action. When exit\n conditions are met then the away action ends and the default\n away action is reapplied.\n ', tunable=TunableTuple(conditions=TunableList(description='\n A list of conditions that all must be satisfied for the\n group to be considered satisfied.\n ', tunable=TunableAwayActionCondition(description='\n A condition for an away action.\n ')))), '_periodic_stat_changes': PeriodicStatisticChange.TunableFactory(description='\n Periodic stat changes that this away action applies while it\n is active.\n '), 'icon': TunableResourceKey(description='\n Icon that represents the away action in on the sim skewer.\n ', default=None, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI), 'tooltip': TunableLocalizedStringFactory(description='\n The tooltip shown on the icon that represents the away action.\n ', tuning_group=GroupNames.UI), 'pie_menu_tooltip': TunableLocalizedStringFactory(description='\n The tooltip shown in the pie menu for this away action.\n ', tuning_group=GroupNames.UI), '_tests': TunableTestSet(description='\n Tests that determine if this away action is applicable. These\n tests do not ensure that the conditions are still met\n throughout the duration that the away action is applied.\n '), '_display_name': TunableLocalizedStringFactory(description='\n The name given to the away action when the user sees it in the\n pie menu.\n ', tuning_group=GroupNames.UI), '_display_name_text_tokens': LocalizationTokens.TunableFactory(description="\n Localization tokens to be passed into 'display_name'.\n For example, you could use a participant or you could also pass\n in statistic and commodity values\n ", tuning_group=GroupNames.UI), '_available_when_instanced': Tunable(description="\n If this away action is able to be applied when the sim is still\n instanced. If the sim becomes instanced while the away action\n is running we will not stop running it.\n \n This should only be true in special cases such as with careers.\n \n PLEASE ASK A GPE ABOUT MAKING THIS TRUE BEFORE DOING SO. YOU\n PROBABLY DON'T WANT THIS.\n ", tunable_type=bool, default=False), '_preroll_commodities': TunableList(description='\n A list of commodities that will be used to run preroll\n if the sim loaded with this away action.\n ', tunable=TunableReference(description='\n The commodity that is used to solve for preroll if the\n sim had this away action on them when they are being loaded.\n \n This is used to help preserve the fiction of what that sim was\n doing when the player returns to the lot. EX: make the sim\n garden if they were using the gardening away action. \n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))), '_preroll_static_commodities': TunableList(description='\n A list of static commodities that will be used to run preroll\n if the sim loaded with this away action.\n ', tunable=StaticCommodity.TunableReference(description='\n The static commodity that is used to solve for preroll if the\n sim had this away action on them when they are being loaded.\n \n This is used to help preserve the fiction of what that sim was\n doing when the player returns to the lot. EX: make the sim\n garden if they were using the gardening away action. \n ')), '_apply_on_load_tags': TunableSet(description='\n A set of tags that are are compared to interaction tags that\n the sim was running when they became uninstantiated. If there\n are any matching tags then this away action will be applied\n automatically to that sim rather than the default away action.\n ', tunable=TunableEnumEntry(description='\n A single tag that will be compared to the interaction tags.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID)), '_disabled_when_running': OptionalTunable(description='\n The availability of this away action when it is already the\n active away action on the sim.\n ', tunable=TunableLocalizedStringFactory(description='\n The text that displays in the tooltip string when this\n away action is not available because it is already the\n active away action.\n '), disabled_name='available_when_running', enabled_name='disabled_when_running'), 'mood_list': TunableList(description='\n A list of possible moods this AwayAction may associate with.\n ', tunable=TunableReference(description='\n A mood associated with this AwayAction.\n ', manager=services.mood_manager()))} def __init__(self, tracker, target=None): self._tracker = tracker self._target = target self._conditional_actions_manager = ConditionalActionManager() self._periodic_stat_changes_instance = self._periodic_stat_changes(self) self._state = AwayActionState.INITIALIZED @classmethod def should_run_on_load(cls, sim_info): for interaction_data in sim_info.si_state.interactions: interaction = services.get_instance_manager(sims4.resources.Types.INTERACTION).get(interaction_data.interaction) if interaction is None: pass while len(interaction.get_category_tags() & cls._apply_on_load_tags) > 0: return True return False @classmethod def get_commodity_preroll_list(cls): if cls._preroll_commodities: return cls._preroll_commodities @classmethod def get_static_commodity_preroll_list(cls): if cls._preroll_static_commodities: return cls._preroll_static_commodities @property def sim_info(self): return self._tracker.sim_info @property def sim(self): return self.sim_info @property def target(self): return self._target @classproperty def available_when_instanced(cls): return cls._available_when_instanced @property def is_running(self): return self._state == AwayActionState.RUNNING def run(self, callback): if self._state == AwayActionState.RUNNING: logger.callstack('Attempting to start away action that is already running.', owner='jjacobson') return self._periodic_stat_changes_instance.run() if self._exit_conditions: self._conditional_actions_manager.attach_conditions(self, self._exit_conditions, callback) self._state = AwayActionState.RUNNING def stop(self): if self._state == AwayActionState.STOPPED: logger.callstack('Attempting to stop away action that is already stopped.', owner='jjacobson') return self._periodic_stat_changes_instance.stop() if self._exit_conditions: self._conditional_actions_manager.detach_conditions(self) self._state = AwayActionState.STOPPED @flexmethod def get_participant(cls, inst, participant_type=ParticipantType.Actor, **kwargs): inst_or_cl = inst if inst is not None else cls participants = inst_or_cl.get_participants(participant_type=participant_type, **kwargs) if not participants: return if len(participants) > 1: raise ValueError('Too many participants returned for {}!'.format(participant_type)) return next(iter(participants)) @flexmethod def get_participants(cls, inst, participant_type, sim_info=DEFAULT, target=DEFAULT) -> set: inst_or_cls = inst if inst is not None else cls sim_info = inst.sim_info if sim_info is DEFAULT else sim_info target = inst.target if target is DEFAULT else target if sim_info is None: logger.error('Sim info is None when trying to get participants for Away Action {}.', inst_or_cls, owner='jjacobson') return () results = set() participant_type = int(participant_type) if participant_type & ParticipantType.Actor: results.add(sim_info) if participant_type & ParticipantType.Lot: zone = services.get_zone(sim_info.zone_id, allow_uninstantiated_zones=True) results.add(zone.lot) if participant_type & ParticipantType.TargetSim and target is not None: results.add(target) return tuple(results) @flexmethod def get_resolver(cls, inst, **away_action_parameters): inst_or_cls = inst if inst is not None else cls return event_testing.resolver.AwayActionResolver(inst_or_cls, **away_action_parameters) @flexmethod def get_localization_tokens(cls, inst, **away_action_parameters): inst_or_cls = inst if inst is not None else cls tokens = inst_or_cls._display_name_text_tokens.get_tokens(inst_or_cls.get_resolver(**away_action_parameters)) return tokens @flexmethod def test(cls, inst, sim_info=DEFAULT, **away_action_parameters): inst_or_cls = inst if inst is not None else cls sim_info = inst.sim_info if sim_info is DEFAULT else sim_info current_away_action = sim_info.current_away_action if inst_or_cls._disabled_when_running and current_away_action is not None and isinstance(current_away_action, cls): return TestResult(False, 'Cannot run away action when it is already running', tooltip=inst_or_cls._disabled_when_running) resolver = inst_or_cls.get_resolver(sim_info=sim_info, **away_action_parameters) if inst is None: condition_actions_manager = ConditionalActionManager() else: condition_actions_manager = inst._conditional_actions_manager if inst_or_cls._exit_conditions and condition_actions_manager.callback_will_trigger_immediately(resolver, inst_or_cls._exit_conditions): return TestResult(False, 'Away Action cannot run since exit conditions will satisfy immediately.') return inst_or_cls._tests.run_tests(resolver) @flexmethod def get_display_name(cls, inst, *tokens, **away_action_parameters): inst_or_cls = inst if inst is not None else cls localization_tokens = inst_or_cls.get_localization_tokens(**away_action_parameters) return inst_or_cls._display_name(*localization_tokens + tokens)
class Photography: SMALL_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a small portrait photo.\n ', manager=services.definition_manager()) SMALL_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a small landscape photo.\n ', manager=services.definition_manager()) MEDIUM_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a medium portrait photo.\n ', manager=services.definition_manager()) MEDIUM_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a medium landscape photo.\n ', manager=services.definition_manager()) LARGE_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a large portrait photo.\n ', manager=services.definition_manager()) LARGE_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a large landscape photo.\n ', manager=services.definition_manager()) PAINTING_INTERACTION_TAG = TunableEnumEntry( description= '\n Tag to specify a painting interaction.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID) PHOTOGRAPHY_LOOT_LIST = TunableList( description= '\n A list of loot operations to apply to the photographer when photo mode exits.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)) FAIL_PHOTO_QUALITY_RANGE = TunableInterval( description= '\n The random quality statistic value that a failure photo will be\n given between the min and max tuned values.\n ', tunable_type=int, default_lower=0, default_upper=100) BASE_PHOTO_QUALITY_MAP = TunableMapping( description= '\n The mapping of CameraQuality value to an interval of quality values\n that will be used to asign a random base quality value to a photo\n as it is created.\n ', key_type=TunableEnumEntry( description= '\n The CameraQuality value. If this photo has this CameraQuality,\n value, then a random quality between the min value and max value\n will be assigned to the photo.\n ', tunable_type=CameraQuality, default=CameraQuality.CHEAP), value_type=TunableInterval( description= '\n The range of base quality values from which a random value will be\n given to the photo.\n ', tunable_type=int, default_lower=1, default_upper=100)) QUALITY_MODIFIER_PER_SKILL_LEVEL = Tunable( description= '\n For each level of skill in Photography, this amount will be added to\n the quality statistic.\n ', tunable_type=float, default=0) PHOTO_VALUE_MODIFIER_MAP = TunableMapping( description= '\n The mapping of state values to Simoleon value modifiers.\n The final value of a photo is decided based on its\n current value multiplied by the sum of all modifiers for\n states that apply to the photo. All modifiers are\n added together first, then the sum will be multiplied by\n the current price.\n ', key_type=TunableStateValueReference( description= '\n The quality state values. If this photo has this state,\n then a random modifier between min_value and max_value\n will be multiplied to the current price.' ), value_type=TunableInterval( description= '\n The maximum modifier multiplied to the current price based on the provided state value\n ', tunable_type=float, default_lower=1, default_upper=1)) PHOTO_VALUE_SKILL_CURVE = TunableStatisticModifierCurve.TunableFactory( description= "\n Allows you to adjust the final value of the photo based on the Sim's\n level of a given skill.\n ", axis_name_overrides=('Skill Level', 'Simoleon Multiplier'), locked_args={'subject': ParticipantType.Actor}) PHOTOGRAPHY_SKILL = Skill.TunablePackSafeReference( description='\n A reference to the photography skill.\n ' ) EMOTION_STATE_MAP = TunableMapping( description= "\n The mapping of moods to states, used to give photo objects a mood\n based state. These states are then used by the tooltip component to\n display emotional content on the photo's tooltip.\n ", key_type=TunableReference( description= '\n The mood to associate with a state.\n ', manager=services.mood_manager()), value_type=TunableStateValueReference( description= '\n The state that represents the mood for the purpose of displaying\n emotional content in a tooltip.\n ' )) PHOTO_OBJECT_LOOT_PER_TARGET = TunableList( description= '\n A list of loots which will be applied once PER target. The participants\n for each application will be Actor: photographer, Target: photograph\n target and Object: the Photograph itself. If a photo interaction has 2\n target sims, this loot will be applied twice.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), pack_safe=True)) MOOD_PARAM_TO_MOOD_CATEGORY_STATE = TunableMapping( description= '\n If the player took a picture in a photo mode that supports mood\n categories, we will perform a state change to the corresponding state\n based on the mood that each picture was taken in.\n ', key_type=Tunable( description= '\n The mood ASM parameter value to associate with a state.\n ', tunable_type=str, default=None), value_type=TunableStateValueReference( description= '\n The state that represents the mood category.\n ' )) GROUP_PHOTO_X_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the x actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) GROUP_PHOTO_Y_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the y actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) GROUP_PHOTO_Z_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the z actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) NUM_PHOTOS_PER_SESSION = Tunable( description= '\n Max possible photos that can be taken during one photo session. Once\n this number has been reached, the photo session will exit.\n ', tunable_type=int, default=5) @classmethod def _is_fail_photo(cls, photo_style_type): if photo_style_type == PhotoStyleType.EFFECT_GRAINY or ( photo_style_type == PhotoStyleType.EFFECT_OVERSATURATED or (photo_style_type == PhotoStyleType.EFFECT_UNDERSATURATED or (photo_style_type == PhotoStyleType.PHOTO_FAIL_BLURRY or (photo_style_type == PhotoStyleType.PHOTO_FAIL_FINGER or photo_style_type == PhotoStyleType.PHOTO_FAIL_GNOME))) ) or photo_style_type == PhotoStyleType.PHOTO_FAIL_NOISE: return True return False @classmethod def _apply_quality_and_value_to_photo(cls, photographer_sim, photo_obj, photo_style, camera_quality): quality_stat = CraftingTuning.QUALITY_STATISTIC quality_stat_tracker = photo_obj.get_tracker(quality_stat) if cls._is_fail_photo(photo_style): final_quality = cls.FAIL_PHOTO_QUALITY_RANGE.random_int() else: quality_range = cls.BASE_PHOTO_QUALITY_MAP.get( camera_quality, None) if quality_range is None: logger.error( 'Photography tuning BASE_PHOTO_QUALITY_MAP does not have an expected quality value: []', str(camera_quality)) return base_quality = quality_range.random_int() skill_quality_modifier = 0 if cls.PHOTOGRAPHY_SKILL is not None: effective_skill_level = photographer_sim.get_effective_skill_level( cls.PHOTOGRAPHY_SKILL) if effective_skill_level: skill_quality_modifier = effective_skill_level * cls.QUALITY_MODIFIER_PER_SKILL_LEVEL final_quality = base_quality + skill_quality_modifier quality_stat_tracker.set_value(quality_stat, final_quality) value_multiplier = 1 for (state_value, value_mods) in cls.PHOTO_VALUE_MODIFIER_MAP.items(): if photo_obj.has_state(state_value.state): actual_state_value = photo_obj.get_state(state_value.state) if state_value is actual_state_value: value_multiplier *= value_mods.random_float() break value_multiplier *= cls.PHOTO_VALUE_SKILL_CURVE.get_multiplier( SingleSimResolver(photographer_sim), photographer_sim) photo_obj.base_value = int(photo_obj.base_value * value_multiplier) @classmethod def _get_mood_sim_info_if_exists(cls, photographer_sim_info, target_sim_ids, camera_mode): if camera_mode is CameraMode.SELFIE_PHOTO: return photographer_sim_info else: num_target_sims = len(target_sim_ids) if num_target_sims == 1: sim_info_manager = services.sim_info_manager() target_sim_info = sim_info_manager.get(target_sim_ids[0]) return target_sim_info @classmethod def _apply_mood_state_if_appropriate(cls, photographer_sim_info, target_sim_ids, camera_mode, photo_object): mood_sim_info = cls._get_mood_sim_info_if_exists( photographer_sim_info, target_sim_ids, camera_mode) if mood_sim_info: mood = mood_sim_info.get_mood() mood_state = cls.EMOTION_STATE_MAP.get(mood, None) if mood_state: photo_object.set_state(mood_state.state, mood_state) @classmethod def _apply_mood_category_state_if_appropriate(cls, selected_mood_param, camera_mode, photo_object): if camera_mode in (CameraMode.TRIPOD, CameraMode.SIM_PHOTO, CameraMode.PHOTO_STUDIO_PHOTO): mood_category_state = cls.MOOD_PARAM_TO_MOOD_CATEGORY_STATE.get( selected_mood_param, None) if mood_category_state: photo_object.set_state(mood_category_state.state, mood_category_state) @classmethod def create_photo_from_photo_data(cls, camera_mode, camera_quality, photographer_sim_id, target_obj_id, target_sim_ids, res_key, photo_style, photo_size, photo_orientation, photographer_sim_info, photographer_sim, time_stamp, selected_mood_param): photo_object = None is_paint_by_reference = camera_mode is CameraMode.PAINT_BY_REFERENCE if is_paint_by_reference: current_zone = services.current_zone() photo_object = current_zone.object_manager.get(target_obj_id) if photo_object is None: photo_object = current_zone.inventory_manager.get( target_obj_id) else: if photo_orientation == PhotoOrientation.LANDSCAPE: if photo_size == PhotoSize.LARGE: photo_object_def = cls.LARGE_LANDSCAPE_OBJ_DEF elif photo_size == PhotoSize.MEDIUM: photo_object_def = cls.MEDIUM_LANDSCAPE_OBJ_DEF elif photo_size == PhotoSize.SMALL: photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF elif photo_orientation == PhotoOrientation.PORTRAIT: if photo_size == PhotoSize.LARGE: photo_object_def = cls.LARGE_PORTRAIT_OBJ_DEF elif photo_size == PhotoSize.MEDIUM: photo_object_def = cls.MEDIUM_PORTRAIT_OBJ_DEF elif photo_size == PhotoSize.SMALL: photo_object_def = cls.SMALL_PORTRAIT_OBJ_DEF else: photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF if photo_object_def is None: return photo_object = create_object(photo_object_def) if photo_object is None: logger.error('photo object could not be found.') return for target_sim_id in target_sim_ids: target_sim_info = services.sim_info_manager().get(target_sim_id) target_sim = target_sim_info.get_sim_instance() resolver = DoubleSimAndObjectResolver(photographer_sim, target_sim, photo_object, source=cls) for loot in cls.PHOTO_OBJECT_LOOT_PER_TARGET: loot.apply_to_resolver(resolver) photography_service = services.get_photography_service() loots = photography_service.get_loots_for_photo() for photoloot in loots: if photoloot._AUTO_FACTORY.FACTORY_TYPE is RotateTargetPhotoLoot: photographer_sim = photoloot.photographer photographer_sim_info = photographer_sim.sim_info break reveal_level = PaintingState.REVEAL_LEVEL_MIN if is_paint_by_reference else PaintingState.REVEAL_LEVEL_MAX painting_state = PaintingState.from_key(res_key, reveal_level, False, photo_style) photo_object.canvas_component.painting_state = painting_state photo_object.canvas_component.time_stamp = time_stamp photo_object.set_household_owner_id(photographer_sim.household_id) if selected_mood_param: cls._apply_mood_category_state_if_appropriate( selected_mood_param, camera_mode, photo_object) if not is_paint_by_reference: cls._apply_quality_and_value_to_photo(photographer_sim, photo_object, photo_style, camera_quality) cls._apply_mood_state_if_appropriate(photographer_sim_info, target_sim_ids, camera_mode, photo_object) photo_object.add_dynamic_component(STORED_SIM_INFO_COMPONENT, sim_id=photographer_sim.id) photo_object.update_object_tooltip() if not (photographer_sim.inventory_component.can_add(photo_object) and photographer_sim.inventory_component. player_try_add_object(photo_object)): logger.error( "photo object could not be put in the sim's inventory, deleting photo." ) photo_object.destroy() photo_targets = [ services.sim_info_manager().get(sim_id) for sim_id in target_sim_ids ] if camera_mode == CameraMode.TWO_SIM_SELFIE_PHOTO: photo_targets.append(photographer_sim_info) photo_targets = frozenset(photo_targets) services.get_event_manager().process_event( test_events.TestEvent.PhotoTaken, sim_info=photographer_sim_info, photo_object=photo_object, photo_targets=photo_targets)
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 Buff(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.BUFF)): __qualname__ = 'Buff' INSTANCE_TUNABLES = {'buff_name': TunableLocalizedString(description='\n Name of buff.\n ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'buff_description': TunableLocalizedString(description='\n Tooltip description of the Buff Effect.\n ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon': TunableResourceKey(description='\n Icon to be displayed for buff\n ', default=None, needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon_highlight': TunableResourceKey(description=" \n Icon to be displayed for when Mood Type is the Sim's active mood.\n ", default=None, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'ui_sort_order': Tunable(description='\n Order buff should be sorted in UI.\n ', tunable_type=int, tuning_group=GroupNames.UI, default=1, export_modes=ExportModes.All), 'visible': Tunable(description='\n Whether this buff should be visible in the UI.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'audio_sting_on_remove': TunableResourceKey(description='\n The sound to play when this buff is removed.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'audio_sting_on_add': TunableResourceKey(description='\n The sound to play when this buff is added.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'show_timeout': Tunable(description='\n Whether timeout should be shown in the UI.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'success_modifier': Tunable(description='\n Base chance delta for interaction success\n ', tunable_type=int, default=0), 'interactions': OptionalTunable(TunableTuple(weight=Tunable(description='\n The selection weight to apply to all interactions added by this\n buff. This takes the place of the SI weight that would be used on\n SuperInteractions.\n ', tunable_type=float, default=1), scored_commodity=statistics.commodity.Commodity.TunableReference(description="\n The commodity that is scored when deciding whether or not to \n perform these interactions. This takes the place of the commodity\n scoring for the SuperInteraction when Subaction Autonomy scores\n all of the SI's in the SI State. If this is None, the default \n value of autonomy.autonomy_modes.SUBACTION_MOTIVE_UTILITY_FALLBACK_SCORE \n will be used.\n "), interaction_items=TunableAffordanceLinkList(description='\n Mixer interactions to add to the Sim when this buff is active.\n ', class_restrictions=(interactions.base.mixer_interaction.MixerInteraction,))), tuning_group=GroupNames.ANIMATION), 'topics': TunableList(description='\n Topics that should be added to sim when buff is added.\n ', tunable=TunableReference(manager=services.topic_manager(), class_restrictions=topics.topic.Topic)), 'game_effect_modifiers': GameEffectModifiers.TunableFactory(description="\n A list of effects that that can modify a Sim's behavior.\n "), 'mood_type': TunableReference(description='\n The mood that this buff pushes onto the owning Sim. If None, does\n not affect mood.\n ', manager=services.mood_manager(), needs_tuning=True, export_modes=ExportModes.All), 'mood_weight': TunableRange(description='\n Weight for this mood. The active mood is determined by summing all\n buffs and choosing the mood with the largest weight.\n ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'proximity_detection_tests': OptionalTunable(description="\n Whether or not this buff should be added because of a Sim's proximity\n to an object with a Proximity Component with this buff in its buffs\n list.\n ", tunable=event_testing.tests.TunableTestSet(description="\n A list of tests groups. At least one must pass all its sub-tests to\n pass the TestSet.\n \n Actor is the one who receives the buff.\n \n If this buff is for two Sims in proximity to each other, only Actor\n and TargetSim should be tuned as Participant Types. Example: A Neat\n Sim is disgusted when around a Sim that has low hygiene. The test\n will be for Actor having the Neat trait and for TargetSim with low\n hygiene motive.\n\n If this buff is for a Sim near an object, only use participant\n types Actor and Object. Example: A Sim who likes classical music\n should get a buff when near a stereo that's playing classical\n music. The test will be for Actor liking classical music and for\n Object in the state of playing classical music.\n "), enabled_by_default=False, disabled_name='no_proximity_detection', enabled_name='proximity_tests'), 'proximity_buff_added_reason': OptionalTunable(tunable=TunableLocalizedString(description="\n If this is a proximity buff, this field will be the reason for why\n the Sim received this buff. Doesn't use tokens.\n "), enabled_by_default=False, disabled_name='no_proximity_add_reason', enabled_name='proximity_add_reason'), '_add_test_set': OptionalTunable(description='\n Whether or not this buff should be added.\n ', tunable=event_testing.tests.TunableTestSet(description='\n A list of tests groups. At least one must pass all its sub-tests to\n pass the TestSet. Only Actor should be tuned as Participant\n Types.The Actor is the Sim that will receive the buff if all tests\n pass."\n '), enabled_by_default=False, disabled_name='always_allowed', enabled_name='tests_set'), 'walkstyle': OptionalTunable(TunableWalkstyle(description="\n A walkstyle override to apply to the Sim while this buff is active.\n Example: you can have Sims with the 'bummed' buff walk in a sad\n fashion.\n "), needs_tuning=True, tuning_group=GroupNames.ANIMATION), 'allow_running_for_long_distance_routes': Tunable(bool, True, description='\n Sims will run when routing long distances outside. Setting this to False\n will disable that functionality. Example: pregnant Sims and walk-by Sims\n should probably never run for this reason.'), 'vfx': OptionalTunable(description='\n vfx to play on the sim when buff is added.\n ', tunable=PlayEffect.TunableFactory(), disabled_name='no_effect', enabled_name='play_effect', tuning_group=GroupNames.ANIMATION), 'static_commodity_to_add': TunableSet(description='\n Static commodity that is added to the sim when buff is added to sim.\n ', tunable=TunableReference(manager=services.static_commodity_manager(), class_restrictions=statistics.static_commodity.StaticCommodity)), '_operating_commodity': statistics.commodity.Commodity.TunableReference(description='\n This is the commodity that is considered the owning commodity of the\n buff. Multiple commodities can reference the same buff. This field\n is used to determine which commodity is considered the authoritative\n commodity. This only needs to be filled if there are more than one\n commodity referencing this buff.\n \n For example, motive_hunger and starvation_commodity both reference\n the same buff. Starvation commodity is marked as the operating\n commodity. If outcome action asks the buff what commodity it should\n apply changes to it will modify the starvation commodity.\n '), '_temporary_commodity_info': OptionalTunable(TunableTuple(description='\n Tunables relating to the generation of a temporary commodity to control\n the lifetime of this buff. If enabled, this buff has no associated\n commodities and will create its own to manage its lifetime.\n ', max_duration=Tunable(description='\n The maximum time buff can last for. Example if set to 100, buff\n only last at max 100 sim minutes. If washing hands gives +10 sim\n minutes for buff. Trying to run interaction for more than 10 times,\n buff time will not increase\n ', tunable_type=int, default=100), categories=TunableSet(description='\n List of categories that this commodity is part of. Used for buff\n removal by category.\n ', tunable=StatisticCategory, needs_tuning=True))), '_appropriateness_tags': TunableSet(description='\n A set of tags that define the appropriateness of the\n interactions allowed by this buff. All SIs are allowed by\n default, so adding this tag generally implies that it is always\n allowed even if another buff has said that it is\n inappropriate.\n ', tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), '_inappropriateness_tags': TunableSet(description="\n A set of tags that define the inappropriateness of the\n interactions allowed by this buff. All SIs are allowed by\n default, so adding this tag generally implies that it's not\n allowed.\n ", tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), 'communicable': OptionalTunable(tunable=LootActions.TunableReference(description='\n The loot to give to Sims that this Sim interacts with while the buff is active.\n This models transmitting the buff, so make sure to tune a percentage chance\n on the loot action to determine the chance of the buff being transmitted.\n ')), '_add_buff_on_remove': OptionalTunable(tunable=TunableBuffReference(description='\n A buff to add to the Sim when this buff is removed.\n ')), '_loot_on_addition': TunableList(description='\n Loot that will be applied when buff is added to sim.\n ', tunable=LootActions.TunableReference()), '_loot_on_removal': TunableList(description='\n Loot that will be applied when buff is removed from sim.\n ', tunable=LootActions.TunableReference()), 'refresh_on_add': Tunable(description='\n This buff will have its duration refreshed if it gets added to a Sim\n who already has the same buff.\n ', tunable_type=bool, needs_tuning=True, default=True), 'flip_arrow_for_progress_update': Tunable(description='\n This only for visible buffs with an owning commodity.\n \n If unchecked and owning commodity is increasing an up arrow will\n appear on the buff and if owning commodity is decreasing a down arrow\n will appear.\n \n If checked and owning commodity is increasing then a down arrow will\n appear on the buff and if owning commodity is decreasing an up arrow\n will appear.\n \n Example of being checked is motive failing buffs, when the commodity is\n increasing we need to show down arrows for the buff.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'timeout_string': TunableLocalizedStringFactory(description='\n String to override the the timeout text. The first token (0.TimeSpan)\n will be the timeout time and the second token will (1.String) will be\n the buff this buff is transitioning to.\n \n If this buff is not transitioning to another buff the only token valid\n in string is 0.Timespan\n \n Example: If this is the hungry buff, then the commodity is decaying to\n starving buff. Normally timeout in tooltip will say \'5 hours\'. With\n this set it will pass in the next buff name as the first token into\n this localized string. So if string provided is \'Becomes {1.String}\n in: {0.TimeSpan}. Timeout tooltip for buff now says \'Becomes Starving\n in: 5 hours\'.\n \n Example: If buff is NOT transitioning into another buff. Localized\n string could be "Great time for :{0.Timespan}". Buff will now say\n "Great time for : 5 hours"\n ', tuning_group=GroupNames.UI, export_modes=(ExportModes.ClientBinary,))} is_mood_buff = False exclusive_index = None exclusive_weight = None trait_replacement_buffs = None _owning_commodity = None @classmethod def _verify_tuning_callback(cls): if cls.visible and not cls.mood_type: logger.error('No mood type set for visible buff: {}. Either provide a mood or make buff invisible.', cls, owner='Tuning') @classmethod def _tuning_loaded_callback(cls): if cls._temporary_commodity_info is not None: if cls._owning_commodity is None: cls._create_temporary_commodity() elif issubclass(cls._owning_commodity, RuntimeCommodity): cls._create_temporary_commodity(proxied_commodity=cls._owning_commodity) def __init__(self, owner, commodity_guid, replacing_buff_type, transition_into_buff_id): self._owner = owner self.commodity_guid = commodity_guid self.effect_modification = self.game_effect_modifiers(owner) self.buff_reason = None self.handle_ids = [] self._static_commodites_added = None self._replacing_buff_type = replacing_buff_type self._mood_override = None self._vfx = None self.transition_into_buff_id = transition_into_buff_id self._walkstyle_active = False @classmethod def _cls_repr(cls): return '{}'.format(cls.__name__) @classmethod def can_add(cls, owner): if cls._add_test_set is not None: resolver = event_testing.resolver.SingleSimResolver(owner) result = cls._add_test_set.run_tests(resolver) if not result: return False return True @classproperty def polarity(cls): if cls.mood_type is not None: return cls.mood_type.buff_polarity return BuffPolarity.NEUTRAL @classproperty def buff_type(cls): return cls @classproperty def get_success_modifier(cls): return cls.success_modifier/100 @classproperty def is_changeable(cls): if cls.mood_type is not None: return cls.mood_type.is_changeable return False @classmethod def add_owning_commodity(cls, commodity): if cls._owning_commodity is None: cls._owning_commodity = commodity elif cls._operating_commodity is None and cls._owning_commodity is not commodity: logger.error('Please fix tuning: Multiple commodities reference {} : commodity:{}, commodity:{}, Set _operating_commodity to authoratative commodity', cls, cls._owning_commodity, commodity) @flexproperty def commodity(cls, inst): if inst is not None and inst._replacing_buff_type is not None: return inst._replacing_buff_type.commodity return cls._operating_commodity or cls._owning_commodity @classmethod def build_critical_section(cls, sim, buff_reason, *sequence): buff_handler = BuffHandler(sim, cls, buff_reason=buff_reason) return build_critical_section_with_finally(buff_handler.begin, sequence, buff_handler.end) @classmethod def _create_temporary_commodity(cls, proxied_commodity=None, create_buff_state=True, initial_value=DEFAULT): if proxied_commodity is None: proxied_commodity = RuntimeCommodity.generate(cls.__name__) proxied_commodity.decay_rate = 1 proxied_commodity.convergence_value = 0 proxied_commodity.remove_on_convergence = True proxied_commodity.visible = False proxied_commodity.max_value_tuning = cls._temporary_commodity_info.max_duration proxied_commodity.min_value_tuning = 0 proxied_commodity.initial_value = initial_value if initial_value is not DEFAULT else cls._temporary_commodity_info.max_duration proxied_commodity._categories = cls._temporary_commodity_info.categories proxied_commodity._time_passage_fixup_type = CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED if create_buff_state: buff_to_add = BuffReference(buff_type=cls) new_state_add_buff = CommodityState(value=0.1, buff=buff_to_add) new_state_remove_buff = CommodityState(value=0, buff=BuffReference()) proxied_commodity.commodity_states = [new_state_remove_buff, new_state_add_buff] cls.add_owning_commodity(proxied_commodity) @classmethod def get_appropriateness(cls, tags): if cls._appropriateness_tags & tags: return Appropriateness.ALLOWED if cls._inappropriateness_tags & tags: return Appropriateness.NOT_ALLOWED return Appropriateness.DONT_CARE @property def mood_override(self): return self._mood_override @mood_override.setter def mood_override(self, value): if not self.is_changeable: logger.error('Trying to override mood for buff:{}, but mood for this is not considered changeable.', self, owner='msantander') self._mood_override = value def on_add(self, load_in_progress): self.effect_modification.on_add() for topic_type in self.topics: self._owner.add_topic(topic_type) tracker = self._owner.static_commodity_tracker for static_commodity_type in self.static_commodity_to_add: tracker.add_statistic(static_commodity_type) if self._static_commodites_added is None: self._static_commodites_added = [] self._static_commodites_added.append(static_commodity_type) self._apply_walkstyle() self.apply_interaction_lockout_to_owner() if not load_in_progress: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: self._start_vfx() if self._loot_on_addition: self._apply_all_loot_actions() def _apply_all_loot_actions(self): sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: resolver = sim.get_resolver() for loot_action in self._loot_on_addition: loot_action.apply_to_resolver(resolver) def on_remove(self, apply_loot_on_remove=True): self.effect_modification.on_remove() for topic_type in self.topics: self._owner.remove_topic(topic_type) if self._static_commodites_added is not None: tracker = self._owner.static_commodity_tracker for static_commodity_type in self._static_commodites_added: tracker.remove_statistic(static_commodity_type) if self._add_buff_on_remove is not None: self._owner.add_buff_from_op(self._add_buff_on_remove.buff_type, self._add_buff_on_remove.buff_reason) self._release_walkstyle() self.on_sim_removed() if apply_loot_on_remove: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: resolver = sim.get_resolver() while True: for loot_action in self._loot_on_removal: loot_action.apply_to_resolver(resolver) def clean_up(self): self.effect_modification.on_remove(on_destroy=True) self._release_walkstyle() self.on_sim_removed() if self._static_commodites_added: self._static_commodites_added.clear() self._static_commodites_added = None def on_sim_ready_to_simulate(self): for topic_type in self.topics: self._owner.add_topic(topic_type) self.apply_interaction_lockout_to_owner() self._start_vfx() def _apply_walkstyle(self): if self.walkstyle is not None and not self._walkstyle_active: self._owner.request_walkstyle(self.walkstyle, id(self)) self._walkstyle_active = True def _release_walkstyle(self): if self._walkstyle_active: self._owner.remove_walkstyle(id(self)) self._walkstyle_active = False def on_sim_removed(self, immediate=False): if self._vfx is not None: self._vfx.stop(immediate=immediate) self._vfx = None def apply_interaction_lockout_to_owner(self): if self.interactions is not None: for mixer_affordance in self.interactions.interaction_items: while mixer_affordance.lock_out_time_initial is not None: self._owner.set_sub_action_lockout(mixer_affordance, initial_lockout=True) def add_handle(self, handle_id, buff_reason=None): self.handle_ids.append(handle_id) self.buff_reason = buff_reason def remove_handle(self, handle_id): if handle_id not in self.handle_ids: return False self.handle_ids.remove(handle_id) if self.handle_ids: return False return True def _start_vfx(self): if self._vfx is None and self.vfx: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) self._vfx = self.vfx(sim) self._vfx.start() def _get_tracker(self): if self.commodity is not None: return self._owner.get_tracker(self.commodity) def _get_commodity_instance(self): if self.commodity is None: return tracker = self._get_tracker() if tracker is None: return commodity_instance = tracker.get_statistic(self.commodity) if commodity_instance is None: return return commodity_instance def _get_absolute_timeout_time(self, commodity_instance, threshold): rate_multiplier = commodity_instance.get_decay_rate_modifier() if rate_multiplier < 1: time = commodity_instance.get_decay_time(threshold) rate_multiplier = 1 else: time = commodity_instance.get_decay_time(threshold, use_decay_modifier=False) if time is not None and time != 0: time_now = services.time_service().sim_now time_stamp = time_now + interval_in_sim_minutes(time) return (time_stamp.absolute_ticks(), rate_multiplier) return NO_TIMEOUT def get_timeout_time(self): commodity_instance = self._get_commodity_instance() if commodity_instance is None: return NO_TIMEOUT buff_type = self.buff_type if self._replacing_buff_type is not None: buff_type = self._replacing_buff_type else: buff_type = self.buff_type state_index = commodity_instance.get_state_index_matches_buff_type(buff_type) if state_index is None: return NO_TIMEOUT state_lower_bound_value = commodity_instance.commodity_states[state_index].value if commodity_instance.convergence_value <= state_lower_bound_value: threshold_value = state_lower_bound_value comparison = operator.le else: comparison = operator.ge next_state_index = state_index + 1 if next_state_index >= len(commodity_instance.commodity_states): threshold_value = commodity_instance.convergence_value else: threshold_value = commodity_instance.commodity_states[next_state_index].value threshold = sims4.math.Threshold(threshold_value, comparison) return self._get_absolute_timeout_time(commodity_instance, threshold)
class BuffTransferOp(BaseTargetedLootOperation): FACTORY_TUNABLES = { 'moods_only': Tunable( description= '\n Checking this box will limit the operations to only the buffs with\n an associated mood.\n ', tunable_type=bool, default=True), '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 ' )), 'mood_types': OptionalTunable( TunableList( description= '\n If enabled, only transfer buffs with associated moods in this list.\n ', tunable=TunableReference(manager=services.mood_manager()))), 'polarity': OptionalTunable( TunableEnumEntry( description= '\n If enabled, only transfer buffs that match the selected polarity.\n ', tunable_type=BuffPolarity, default=BuffPolarity.NEUTRAL, tuning_group=GroupNames.UI)) } def __init__(self, moods_only, buff_reason, mood_types=None, polarity=None, **kwargs): super().__init__(**kwargs) self._moods_only = moods_only self._buff_reason = buff_reason self._mood_types = mood_types self._polarity = polarity def _apply_to_subject_and_target(self, subject, target, resolver): old_buff_types = list(subject.get_active_buff_types()) if self._moods_only: for buff_entry in old_buff_types: if buff_entry.mood_type is not None: subject.remove_buff_by_type(buff_entry) else: for buff_entry in old_buff_types: subject.remove_buff_by_type(buff_entry) for target_buff in target.get_active_buff_types(): if self._moods_only and target_buff.mood_type is None: continue if self._mood_types is not None and target_buff.mood_type not in self._mood_types: continue if self._polarity is not None and self._polarity is not target_buff.polarity: continue buff_commodity = target_buff.commodity subject.add_buff(target_buff) if buff_commodity is not None: tracker = subject.get_tracker(buff_commodity) tracker.set_max(buff_commodity) subject.set_buff_reason(target_buff, self._buff_reason)
class BuffComponent(objects.components.Component, component_name=objects.components.types.BUFF_COMPONENT): __qualname__ = 'BuffComponent' DEFAULT_MOOD = TunableReference(services.mood_manager(), description='The default initial mood.') UPDATE_INTENSITY_BUFFER = TunableRange(description="\n A buffer that prevents a mood from becoming active unless its intensity\n is greater than the current active mood's intensity plus this amount.\n \n For example, if this tunable is 1, and the Sim is in a Flirty mood with\n intensity 2, then a different mood would become the active mood only if\n its intensity is 3+.\n \n If the predominant mood has an intensity that is less than the active\n mood's intensity, that mood will become the active mood.\n ", tunable_type=int, default=1, minimum=0) EXCLUSIVE_SET = TunableList(description='\n A list of buff groups to determine which buffs are exclusive from each\n other within the same group. A buff cannot exist in more than one exclusive group.\n \n The following rule of exclusivity for a group:\n 1. Higher weight will always be added and remove any lower weight buffs\n 2. Lower weight buff will not be added if a higher weight already exist in component\n 3. Same weight buff will always be added and remove any buff with same weight.\n \n Example: Group 1:\n Buff1 with weight of 5 \n Buff2 with weight of 1\n Buff3 with weight of 1\n Group 2:\n Buff4 with weight of 6\n \n If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n If sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n If sim has Buff4, trying to add Buff1, Buff2, or Buff3 will be added and Buff4 will stay \n on component \n ', tunable=TunableList(tunable=TunableTuple(buff_type=TunableReference(description='\n Buff in exclusive group\n ', manager=services.get_instance_manager(sims4.resources.Types.BUFF)), weight=Tunable(description='\n weight to determine if this buff should be added and\n remove other buffs in the exclusive group or not added at all.\n \n Example: Buff1 with weight of 5 \n Buff2 with weight of 1\n Buff3 with weight of 1\n \n If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n if sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n ', tunable_type=int, default=1)))) def __init__(self, owner): super().__init__(owner) self._active_buffs = {} self._get_next_handle_id = UniqueIdGenerator() self._success_chance_modification = 0 self._active_mood = self.DEFAULT_MOOD self._active_mood_intensity = 0 self._active_mood_buff_handle = None self.on_mood_changed = CallableList() self.on_mood_changed.append(self._publish_mood_update) self.on_mood_changed.append(self._send_mood_changed_event) self.load_in_progress = False self.on_buff_added = CallableList() self.on_buff_removed = CallableList() self.buff_update_alarms = {} if self._active_mood is None: logger.error('No default mood tuned in buff_component.py') elif self._active_mood.buffs: initial_buff_ref = self._active_mood.buffs[0] if initial_buff_ref and initial_buff_ref.buff_type: self._active_mood_buff_handle = self.add_buff(initial_buff_ref.buff_type) def __iter__(self): return self._active_buffs.values().__iter__() def __len__(self): return len(self._active_buffs) def on_sim_ready_to_simulate(self): for buff in self: buff.on_sim_ready_to_simulate() self._publish_mood_update() def on_sim_removed(self, *args, **kwargs): for buff in self: buff.on_sim_removed(*args, **kwargs) def clean_up(self): for (buff_type, buff_entry) in tuple(self._active_buffs.items()): self.remove_auto_update(buff_type) buff_entry.clean_up() self._active_buffs.clear() self.on_mood_changed.clear() self.on_buff_added.clear() self.on_buff_removed.clear() @objects.components.componentmethod def add_buff_from_op(self, buff_type, buff_reason=None): (can_add, _) = self._can_add_buff_type(buff_type) if not can_add: return False buff_commodity = buff_type.commodity if buff_commodity is not None: if not buff_type.refresh_on_add and self.has_buff(buff_type): return False tracker = self.owner.get_tracker(buff_commodity) if buff_commodity.convergence_value == buff_commodity.max_value: tracker.set_min(buff_commodity) else: tracker.set_max(buff_commodity) self.set_buff_reason(buff_type, buff_reason, use_replacement=True) else: self.add_buff(buff_type, buff_reason=buff_reason) return True @objects.components.componentmethod def add_buff(self, buff_type, buff_reason=None, update_mood=True, commodity_guid=None, replacing_buff=None, timeout_string=None, transition_into_buff_id=0, change_rate=None, immediate=False): replacement_buff_type = self._get_replacement_buff_type(buff_type) if replacement_buff_type is not None: return self.owner.add_buff(replacement_buff_type, buff_reason=buff_reason, update_mood=update_mood, commodity_guid=commodity_guid, replacing_buff=buff_type, timeout_string=timeout_string, transition_into_buff_id=transition_into_buff_id, change_rate=change_rate, immediate=immediate) (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type) if not can_add: return buff = self._active_buffs.get(buff_type) if buff is None: buff = buff_type(self.owner, commodity_guid, replacing_buff, transition_into_buff_id) self._active_buffs[buff_type] = buff buff.on_add(self.load_in_progress) self._update_chance_modifier() if update_mood: self._update_current_mood() if self.owner.household is not None: services.get_event_manager().process_event(test_events.TestEvent.BuffBeganEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type) self.register_auto_update(self.owner, buff_type) self.on_buff_added(buff_type) handle_id = self._get_next_handle_id() buff.add_handle(handle_id, buff_reason=buff_reason) self.send_buff_update_msg(buff, True, change_rate=change_rate, immediate=immediate) if conflicting_buff_type is not None: self.remove_buff_by_type(conflicting_buff_type) return handle_id def _get_replacement_buff_type(self, buff_type): if buff_type.trait_replacement_buffs is not None: trait_tracker = self.owner.trait_tracker for (trait, replacement_buff_type) in buff_type.trait_replacement_buffs.items(): while trait_tracker.has_trait(trait): return replacement_buff_type def register_auto_update(self, sim_info_in, buff_type_in): if buff_type_in in self.buff_update_alarms: self.remove_auto_update(buff_type_in) if sim_info_in.is_selectable and buff_type_in.visible: self.buff_update_alarms[buff_type_in] = alarms.add_alarm(self, create_time_span(minutes=15), lambda _, sim_info=sim_info_in, buff_type=buff_type_in: services.get_event_manager().process_event(test_events.TestEvent.BuffUpdateEvent, sim_info=sim_info, sim_id=sim_info.sim_id, buff=buff_type), True) def remove_auto_update(self, buff_type): if buff_type in self.buff_update_alarms: alarms.cancel_alarm(self.buff_update_alarms[buff_type]) del self.buff_update_alarms[buff_type] @objects.components.componentmethod def remove_buff(self, handle_id, update_mood=True, immediate=False, on_destroy=False): for (buff_type, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: should_remove = buff_entry.remove_handle(handle_id) if should_remove: del self._active_buffs[buff_type] buff_entry.on_remove(not self.load_in_progress and not on_destroy) if not on_destroy: if update_mood: self._update_current_mood() self._update_chance_modifier() self.send_buff_update_msg(buff_entry, False, immediate=immediate) services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type) if buff_type in self.buff_update_alarms: self.remove_auto_update(buff_type) self.on_buff_removed(buff_type) break @objects.components.componentmethod def get_buff_type(self, handle_id): for (buff_type, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: return buff_type @objects.components.componentmethod def has_buff(self, buff_type): return buff_type in self._active_buffs @objects.components.componentmethod def get_active_buff_types(self): return self._active_buffs.keys() @objects.components.componentmethod def get_buff_reason(self, handle_id): for buff_entry in self._active_buffs.values(): while handle_id in buff_entry.handle_ids: return buff_entry.buff_reason @objects.components.componentmethod def debug_add_buff_by_type(self, buff_type): (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type) if not can_add: return False if buff_type.commodity is not None: tracker = self.owner.get_tracker(buff_type.commodity) state_index = buff_type.commodity.get_state_index_matches_buff_type(buff_type) if state_index is not None: index = state_index + 1 if index < len(buff_type.commodity.commodity_states): commodity_to_value = buff_type.commodity.commodity_states[index].value - 1 else: commodity_to_value = buff_type.commodity.max_value tracker.set_value(buff_type.commodity, commodity_to_value) else: logger.error('commodity ({}) has no states with buff ({}), Buff will not be added.', buff_type.commodity, buff_type) return False else: self.add_buff(buff_type) if conflicting_buff_type is not None: self.remove_buff_by_type(conflicting_buff_type) return True @objects.components.componentmethod def remove_buff_by_type(self, buff_type, on_destroy=False): buff_entry = self._active_buffs.get(buff_type) self.remove_buff_entry(buff_entry, on_destroy=on_destroy) @objects.components.componentmethod def remove_buff_entry(self, buff_entry, on_destroy=False): if buff_entry is not None: if buff_entry.commodity is not None: tracker = self.owner.get_tracker(buff_entry.commodity) commodity_inst = tracker.get_statistic(buff_entry.commodity) if commodity_inst is not None and commodity_inst.core: if not on_destroy: logger.callstack('Attempting to explicitly remove the buff {}, which is given by a core commodity. This would result in the removal of a core commodity and will be ignored.', buff_entry, owner='tastle', level=sims4.log.LEVEL_ERROR) return tracker.remove_statistic(buff_entry.commodity, on_destroy=on_destroy) elif buff_entry.buff_type in self._active_buffs: buff_entry.on_remove(on_destroy) del self._active_buffs[buff_entry.buff_type] if not on_destroy: self._update_chance_modifier() self._update_current_mood() self.send_buff_update_msg(buff_entry, False) services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, buff=type(buff_entry), sim_id=self.owner.id) @objects.components.componentmethod def set_buff_reason(self, buff_type, buff_reason, use_replacement=False): if use_replacement: replacement_buff_type = self._get_replacement_buff_type(buff_type) if replacement_buff_type is not None: buff_type = replacement_buff_type buff_entry = self._active_buffs.get(buff_type) if buff_entry is not None and buff_reason is not None: buff_entry.buff_reason = buff_reason self.send_buff_update_msg(buff_entry, True) @objects.components.componentmethod def buff_commodity_changed(self, handle_id, change_rate=None): for (_, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: if buff_entry.show_timeout: self.send_buff_update_msg(buff_entry, True, change_rate=change_rate) break @objects.components.componentmethod def get_success_chance_modifier(self): return self._success_chance_modification @objects.components.componentmethod def get_actor_scoring_modifier(self, affordance): total = 0 for buff_entry in self._active_buffs.values(): total += buff_entry.effect_modification.get_affordance_scoring_modifier(affordance) return total @objects.components.componentmethod def get_actor_success_modifier(self, affordance): total = 0 for buff_entry in self._active_buffs.values(): total += buff_entry.effect_modification.get_affordance_success_modifier(affordance) return total @objects.components.componentmethod def get_mood(self): return self._active_mood @objects.components.componentmethod def get_mood_animation_param_name(self): param_name = self._active_mood.asm_param_name if param_name is not None: return param_name (mood, _, _) = self._get_largest_mood(predicate=lambda mood: return True if mood.asm_param_name else False) return mood.asm_param_name @objects.components.componentmethod def get_mood_intensity(self): return self._active_mood_intensity @objects.components.componentmethod def get_effective_skill_level(self, skill): if skill.stat_type == skill: skill = self.owner.get_stat_instance(skill) if skill is None: return 0 modifier = 0 for buff_entry in self._active_buffs.values(): modifier += buff_entry.effect_modification.get_effective_skill_modifier(skill) return skill.get_user_value() + modifier @objects.components.componentmethod def effective_skill_modified_buff_gen(self, skill): if skill.stat_type == skill: skill = self.owner.get_stat_instance(skill) for buff_entry in self._active_buffs.values(): modifier = buff_entry.effect_modification.get_effective_skill_modifier(skill) while modifier != 0: yield (buff_entry, modifier) @objects.components.componentmethod def is_appropriate(self, tags): final_appropriateness = Appropriateness.DONT_CARE for buff in self._active_buffs: appropriateness = buff.get_appropriateness(tags) while appropriateness > final_appropriateness: final_appropriateness = appropriateness if final_appropriateness == Appropriateness.NOT_ALLOWED: return False return True def get_additional_create_ops_gen(self): yield GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg()) for buff in self: while buff.visible: yield GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, self._create_buff_update_msg(buff, True)) def _publish_mood_update(self): if self.owner.valid_for_distribution and self.owner.visible_to_client == True: Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg())) def _send_mood_changed_event(self): if not self.owner.is_npc: self.owner.whim_tracker.refresh_emotion_whim() services.get_event_manager().process_event(test_events.TestEvent.MoodChange, sim_info=self.owner) def _create_mood_update_msg(self): mood_msg = Commodities_pb2.MoodUpdate() mood_msg.sim_id = self.owner.id mood_msg.mood_key = self._active_mood.guid64 mood_msg.mood_intensity = self._active_mood_intensity return mood_msg def _create_buff_update_msg(self, buff, equipped, change_rate=None): buff_msg = Sims_pb2.BuffUpdate() buff_msg.buff_id = buff.guid64 buff_msg.sim_id = self.owner.id buff_msg.equipped = equipped if buff.buff_reason is not None: buff_msg.reason = buff.buff_reason if equipped and buff.show_timeout: (timeout, rate_multiplier) = buff.get_timeout_time() buff_msg.timeout = timeout buff_msg.rate_multiplier = rate_multiplier if change_rate is not None: if change_rate == 0: progress_arrow = Sims_pb2.BUFF_PROGRESS_NONE elif change_rate > 0: progress_arrow = Sims_pb2.BUFF_PROGRESS_UP if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_DOWN else: progress_arrow = Sims_pb2.BUFF_PROGRESS_DOWN if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_UP buff_msg.buff_progress = progress_arrow buff_msg.is_mood_buff = buff.is_mood_buff buff_msg.commodity_guid = buff.commodity_guid or 0 if buff.mood_override is not None: buff_msg.mood_type_override = buff.mood_override.guid64 buff_msg.transition_into_buff_id = buff.transition_into_buff_id return buff_msg def send_buff_update_msg(self, buff, equipped, change_rate=None, immediate=False): if not buff.visible: return if self.owner.valid_for_distribution and self.owner.is_sim and self.owner.is_selectable: buff_msg = self._create_buff_update_msg(buff, equipped, change_rate=change_rate) if gsi_handlers.buff_handlers.sim_buff_log_archiver.enabled: gsi_handlers.buff_handlers.archive_buff_message(buff_msg, equipped, change_rate) Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, buff_msg)) def _can_add_buff_type(self, buff_type): if not buff_type.can_add(self.owner): return (False, None) mood = buff_type.mood_type if mood is not None and mood.excluding_traits is not None and self.owner.trait_tracker.has_any_trait(mood.excluding_traits): return (False, None) if buff_type.exclusive_index is None: return (True, None) for conflicting_buff_type in self._active_buffs: while conflicting_buff_type.exclusive_index == buff_type.exclusive_index: if buff_type.exclusive_weight < conflicting_buff_type.exclusive_weight: return (False, None) return (True, conflicting_buff_type) return (True, None) def _update_chance_modifier(self): positive_success_buff_delta = 0 negative_success_buff_delta = 1 for buff_entry in self._active_buffs.values(): if buff_entry.success_modifier > 0: positive_success_buff_delta += buff_entry.get_success_modifier else: negative_success_buff_delta *= 1 + buff_entry.get_success_modifier self._success_chance_modification = positive_success_buff_delta - (1 - negative_success_buff_delta) def _get_largest_mood(self, predicate=None, buffs_to_ignore=()): weights = {} polarity_to_changeable_buffs = collections.defaultdict(list) polarity_to_largest_mood_and_weight = {} for buff_entry in self._active_buffs.values(): current_mood = buff_entry.mood_type current_weight = buff_entry.mood_weight while not current_mood is None: if current_weight == 0: pass if not (predicate is not None and predicate(current_mood)): pass if buff_entry in buffs_to_ignore: pass current_polarity = current_mood.buff_polarity if buff_entry.is_changeable: polarity_to_changeable_buffs[current_polarity].append(buff_entry) total_current_weight = weights.get(current_mood, 0) total_current_weight += current_weight weights[current_mood] = total_current_weight (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(current_polarity, (None, None)) if largest_mood is None: polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight) else: while total_current_weight > largest_weight: polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight) all_changeable_buffs = [] for (buff_polarity, changeable_buffs) in polarity_to_changeable_buffs.items(): (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(buff_polarity, (None, None)) if largest_mood is not None: for buff_entry in changeable_buffs: if buff_entry.mood_override is not largest_mood: all_changeable_buffs.append((buff_entry, largest_mood)) largest_weight += buff_entry.mood_weight polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight) else: weights = {} largest_weight = 0 for buff_entry in changeable_buffs: if buff_entry.mood_override is not None: all_changeable_buffs.append((buff_entry, None)) current_mood = buff_entry.mood_type current_weight = buff_entry.mood_weight total_current_weight = weights.get(current_mood, 0) total_current_weight += current_weight weights[current_mood] = total_current_weight while total_current_weight > largest_weight: largest_weight = total_current_weight largest_mood = current_mood while largest_mood is not None and largest_weight != 0: polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight) largest_weight = 0 largest_mood = self.DEFAULT_MOOD active_mood = self._active_mood if polarity_to_largest_mood_and_weight: (mood, weight) = max(polarity_to_largest_mood_and_weight.values(), key=operator.itemgetter(1)) if weight > largest_weight or weight == largest_weight and mood is active_mood: largest_weight = weight largest_mood = mood return (largest_mood, largest_weight, all_changeable_buffs) def _update_current_mood(self): (largest_mood, largest_weight, changeable_buffs) = self._get_largest_mood() if largest_mood is not None: intensity = self._get_intensity_from_mood(largest_mood, largest_weight) if self._should_update_mood(largest_mood, intensity, changeable_buffs): if self._active_mood_buff_handle is not None: active_mood_buff_handle = self._active_mood_buff_handle self.remove_buff(active_mood_buff_handle, update_mood=False) if active_mood_buff_handle == self._active_mood_buff_handle: self._active_mood_buff_handle = None else: return self._active_mood = largest_mood self._active_mood_intensity = intensity if len(largest_mood.buffs) >= intensity: tuned_buff = largest_mood.buffs[intensity] if tuned_buff is not None and tuned_buff.buff_type is not None: self._active_mood_buff_handle = self.add_buff(tuned_buff.buff_type, update_mood=False) if gsi_handlers.buff_handlers.sim_mood_log_archiver.enabled and self.owner.valid_for_distribution and self.owner.visible_to_client == True: gsi_handlers.buff_handlers.archive_mood_message(self.owner.id, self._active_mood, self._active_mood_intensity, self._active_buffs, changeable_buffs) caches.clear_all_caches() self.on_mood_changed() for (changeable_buff, mood_override) in changeable_buffs: changeable_buff.mood_override = mood_override self.send_buff_update_msg(changeable_buff, True) def _get_intensity_from_mood(self, mood, weight): intensity = 0 for threshold in mood.intensity_thresholds: if weight >= threshold: intensity += 1 else: break return intensity def _should_update_mood(self, mood, intensity, changeable_buffs): active_mood = self._active_mood active_mood_intensity = self._active_mood_intensity if mood is active_mood: return intensity != active_mood_intensity total_weight = sum(buff_entry.mood_weight for buff_entry in self._active_buffs.values() if buff_entry.mood_type is active_mood) active_mood_intensity = self._get_intensity_from_mood(active_mood, total_weight) if changeable_buffs and not self._active_mood.is_changeable: buffs_to_ignore = [changeable_buff for (changeable_buff, _) in changeable_buffs] (largest_mood, largest_weight, _) = self._get_largest_mood(buffs_to_ignore=buffs_to_ignore) new_intensity = self._get_intensity_from_mood(largest_mood, largest_weight) if self._should_update_mood(largest_mood, new_intensity, None): active_mood = largest_mood active_mood_intensity = new_intensity if active_mood.is_changeable and mood.buff_polarity == active_mood.buff_polarity: return True if not intensity or intensity < active_mood_intensity: return True if intensity >= active_mood_intensity + self.UPDATE_INTENSITY_BUFFER: return True if mood is self.DEFAULT_MOOD or active_mood is self.DEFAULT_MOOD: return True return False
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
class Recipe(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RECIPE)): __qualname__ = 'Recipe' INSTANCE_TUNABLES = { 'name': TunableLocalizedStringFactory( description='\n The name of this recipe.\n ' ), 'phase_interaction_name': TunableLocalizedStringFactory( description= "\n The name of each phase's interaction.\n " ), 'final_product': TunableRecipeObjectInfo( description= '\n The final product of the crafting process.\n ', optional_create=False), '_first_phases': TunableList( description= '\n The names of the phases that can be done first. This cannot be empty.\n ', tunable=TunableEnumEntry(PhaseName, default=None)), '_phases': TunableMapping( description= '\n The phases that make up this recipe.\n ', key_type=TunableEnumEntry(PhaseName, None), value_type=TunablePhaseVariant()), 'resume_affordance': OptionalTunable( description= '\n The interaction to use when resuming crafting this recipe.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), class_restrictions=('CraftingResumeInteraction', ))), 'skill_test': OptionalTunable( description= '\n The skill level required to use this recipe.\n ', tunable=event_testing.test_variants.SkillRangeTest.TunableFactory( )), 'additional_tests': event_testing.tests.TunableTestSetWithTooltip(), 'additional_tests_ignored_on_resume': Tunable( description= '\n If set, additional tests are ignored when testing to see if a recipe\n can be resumed.\n ', tunable_type=bool, default=False), 'utility_info': OptionalTunable(TunableList( description= '\n Tuning that specifies which utilities this recipe requires to\n be made/cooked.\n ', tunable=TunableEnumEntry(Utilities, None)), needs_tuning=True), '_retail_price': Tunable( description= '\n Retail price of the recipe. \n This is not the total price of the recipe. The total price of the \n recipe will be _retail_price+delta_ingredient_price\n ', tunable_type=int, default=0), 'crafting_cost': TunableVariant( description= '\n This determines the amount of money the Sim will have to pay in\n order to craft this recipe.\n \n Crafting Discount is multiplied by the Retail Price to determine the\n amount of money the Sim must pay to craft this.\n \n Flat Fee is just a flat fee the Sim must pay to craft this.\n ', discount=Tunable( description= '\n This number is multiplied by the Retail Price to to determine\n the amount of money the Sim will have to pay in order to craft this.\n ', tunable_type=float, default=0.8), flat_fee=Tunable( description= '\n This is a flat amount of simoleons that the Sim will have to pay\n in order to craft this.\n ', tunable_type=int, default=0), default='discount'), 'delta_ingredient_price': Tunable( description= '\n Delta price of a recipe will be a delta value that will increase\n or decrease depending on how many ingredients for the recipe the \n sim has.\n e.g For a 3 ingredient recipe:\n - If no ingredient is found, price will be retail_price + delta\n - If 1 ingredient is found, price will be retail_price + 2/3 of \n delta\n - If 2 ingredient is found, price will be retail_price + 1/3 of \n delta\n - If 3 ingredient is found, price will be retail_price\n ', tunable_type=int, default=0), '_no_initial_charge': Tunable( description= '\n If set, there is no initial charge for making this recipe\n ', tunable_type=bool, needs_tuning=True, default=False), 'icon_override': TunableResourceKey( description= "\n This will override the default icon for this recipe. If left blank, the thumbnail of the recipe's final product will be used.\n ", default=None, resource_types=sims4.resources.CompoundTypes.IMAGE), 'recipe_description': TunableLocalizedStringFactory(description='recipe description'), 'autonomy_weight': Tunable( description= '\n The relative weight for autonomy to choose to make or order this recipe\n ', tunable_type=float, default=0), 'buff_weight_multipliers': TunableBuffWeightMultipliers(), 'base_recipe': OptionalTunable( description= '\n If set, the single serving counterpart for this recipe\n ', tunable=TunableReference(services.recipe_manager())), 'visible_as_subrow': Tunable( description= '\n If this recipe has base recipe, this boolean will decide whether or\n not this recipe will show up in the subrow entry of the base\n recipe.\n ', tunable_type=bool, default=True), 'base_recipe_category': OptionalTunable( description= '\n The pie menu category of the base recipe if the picker dialog to\n choose recipes is tuned to use pie menu formation.\n ', tunable=TunableReference( description= '\n Pie menu category for pie menu mixers.\n ', manager=services.get_instance_manager( sims4.resources.Types.PIE_MENU_CATEGORY))), 'multi_serving_name': OptionalTunable( TunableLocalizedStringFactory( description= 'The name of multiple serving to show in the sub row of ObjectPicker.' ), enabled_name ='use_multi_serving_name', disabled_name='use_recipe_name', description= "The string that shows up in the ObjectPicker when this recipe is a multi_serving. Please use 'use_recipe_name' when it's a single serving" ), 'push_consume': Tunable( description= '\n Whether to push the consume after finish the recipe.\n ', tunable_type=bool, needs_tuning=True, default=True), 'push_consume_threshold': OptionalTunable( description= '\n If Push Consume is checked and this threshold is enabled, the consume affordance will\n only be pushed if the threshold is met.\n ', tunable=TunableTuple( description= '\n The commodity/threshold pair.\n ', commodity=TunableReference( description= '\n The commodity to be tested.\n ', manager=services.statistic_manager(), class_restrictions='Commodity'), threshold=TunableThreshold( description= '\n The threshold at which to remove this bit.\n ' ))), 'skill_loot_data': TunableSkillLootData( description= 'Loot Data for DynamicSkillLootOp. This will only be used if in the loot list of the outcome there is a dynamic loot op.' ), 'masterwork_name': TunableLocalizedString( description= 'If this can be a masterwork, what should its masterwork state be called? Usually this is just Masterpiece.' ), 'resumable': Tunable( description= '\n If set to False, this recipe is not resumable, for example drinks made at bar.\n ', tunable_type=bool, default=True), 'resumable_by_different_sim': Tunable( description= '\n If set, this recipe can be resumed by a sim other than the one that started it\n ', tunable_type=bool, needs_tuning=True, default=False), 'entitlement': TunableEntitlement( description= '\n Entitlement required to use this recipe.\n ' ), 'hidden_until_unlock': Tunable( description= "\n If checked, this recipe will not show up in picker or piemenu if\n it's not unlocked, either by skill up, or unlock outcome, or\n entitlement.\n ", tunable_type=bool, needs_tuning=True, default=True), 'use_ingredients': OptionalTunable( description= '\n If checked recipe will have the ability to use ingredients to \n improve its quality when prepared.\n ', tunable=TunableTuple( description= '\n Ingredient data for a recipe.\n ', all_ingredients_required=Tunable( description= '\n If checked recipe will not be available unless all \n ingredients are found on the sim inventory or the fridge\n inventory.\n ', tunable_type=bool, default=False), ingredient_list=TunableList( description= '\n List of ingredients the recipe can use\n ', tunable=TunableVariant( description= '\n Possible ingredient mapping by object definition of by \n catalog object Tag.\n ', ingredient_by_definition=TunableIngredientByDefFactory( ), ingredient_by_tag=TunableIngredientByTagFactory())))), 'crafted_by_text': TunableLocalizedStringFactory( description= "\n Text that describes who made it, e.g. 'Made By: <Sim>'. Loc\n parameter 0 is the Sim crafter.\n " ), 'value_text': TunableLocalizedStringFactory( description= "\n Text (if any) that describes the value in the tooltip. \n e.g. 'Value: $500'. Loc parameter 0 is the value.\n " ), 'mood_list': TunableList( description= '\n A list of possible moods this Recipe may associate with. Note that\n this list is for coloring-purposes only. Interaction availability\n testing is relegated to the individual mood tests on the\n interaction. Future refactoring necessary.\n ', tunable=TunableReference(manager=services.mood_manager())) } _tuning_loaded = False _referenced_by_start_crafting = False @classmethod def _tuning_loaded_callback(cls): cls._visible_phase_sequence = [] cls.phases = { phase_id: phase(recipe=cls, phase_id=phase_id) for (phase_id, phase) in cls._phases.items() } first_phases = [] for name in cls._first_phases: if name in cls.phases: first_phases.append(cls.phases[name]) else: logger.error( "Unknown phase '{}' specified in first_phases for recipe '{}'.", name, cls) cls.first_phases = first_phases for phase in cls.phases.values(): phase.recipe_tuning_loaded() if cls.first_phases: cls._build_visible_phase_sequence(cls.first_phases[0]) cls._tuning_loaded = True if cls._referenced_by_start_crafting: cls.validate_for_start_crafting() @classmethod def _verify_tuning_callback(cls): if cls.use_ingredients: for ingredient in cls.use_ingredients.ingredient_list: while ingredient is None: logger.error('Recipe {} has unset ingredients', cls.__name__) if not cls.use_ingredients.ingredient_list: logger.error( 'Recipe {} has an empty ingredient list and its tuned to use ingredients.', cls.__name__) if not cls.name: logger.error('Recipe {} does not have a name set', cls.__name__) if not cls.phase_interaction_name: logger.error( 'Recipe {} does not have a phase interaction name set', cls.__name__) cls._validate_final_product() services.get_instance_manager( sims4.resources.Types.RECIPE).add_on_load_complete( cls.validate_base_recipe) @classmethod def _validate_final_product(cls): if cls.final_product_definition is None: return supported_states = objects.components.state.get_supported_state( cls.final_product.definition) unsupported_values = [] for state_value in itertools.chain(cls.final_product.initial_states, cls.final_product.apply_states): while supported_states is None or state_value.state not in supported_states: unsupported_values.append(state_value) if supported_states is None: error = "\n A recipe wants to set one or more state value on its final product, but that\n object doesn't have a StateComponent. The recipe shouldn't be trying to set\n any state values, or the object's tuning should be updated to add these\n states." else: error = "\n A recipe wants to set a state value on its final product, but that object's\n state component tuning doesn't have an entry for that state. The recipe\n shouldn't be trying to set these state values, or the object's tuning should\n be updated to add these states." logger.warn( 'Recipe tuning error:{}\n Recipe: {}\n Missing States: {}\n Final Product: {} ({})' .format( error, cls.__name__, ', '.join( sorted({e.state.__name__ for e in unsupported_values})), cls.final_product.definition.name, cls.final_product.definition.cls.__name__)) for sa in cls.final_product.super_affordances: while sa.consumes_object() or sa.contains_stat( CraftingTuning.CONSUME_STATISTIC): logger.error( 'Recipe: Interaction {} on {} is consume affordance, should tune on ConsumableComponent of the object.', sa.__name__, cls.__name__, owner='tastle/cjiang') @classmethod def validate_for_start_crafting(cls): if not cls._tuning_loaded: cls._referenced_by_start_crafting = True elif not cls.first_phases: logger.error( 'Recipe is tuned to be craftable but has no first phases defined: {}', cls) @classmethod def validate_base_recipe(cls, manager): base_recipe = cls.base_recipe if base_recipe is not None and cls.hidden_until_unlock != base_recipe.hidden_until_unlock: logger.error( "Recipe({})'s hidden_until_unlock({}) != base_recipe({})'s hidden_until_unlock({})", cls.__name__, cls.hidden_until_unlock, base_recipe.__name__, base_recipe.hidden_until_unlock) @classmethod def _build_visible_phase_sequence(cls, phase): if phase.is_visible: cls._visible_phase_sequence.append(phase) if phase.next_phases: next_phase = phase.next_phases[0] cls._build_visible_phase_sequence(next_phase) @classproperty def final_product_definition(cls): return cls.final_product.definition @classproperty def final_product_definition_id(cls): return cls.final_product.definition.id @classproperty def has_final_product_definition(cls): return cls.final_product.definition is not None @classproperty def final_product_geo_hash(cls): if cls.final_product.thumbnail_geo_state is not None: return sims4.hash_util.hash32( cls.final_product.thumbnail_geo_state) return cls.final_product_definition.thumbnail_geo_state_hash @classproperty def final_product_material_hash(cls): if cls.final_product.thumbnail_material_state is not None: return sims4.hash_util.hash32( cls.final_product.thumbnail_material_state) return 0 @classproperty def final_product_type(cls): return cls.final_product.definition.cls @classproperty def apply_tags(cls): obj_info = cls.final_product if obj_info is not None: return obj_info.apply_tags return set() @classmethod def get_final_product_quality_adjustment(cls, effective_skill): quality_adjustment = cls.final_product.quality_adjustment skill_delta = effective_skill - cls.required_skill_level quality_value = quality_adjustment.base_quality + skill_delta * quality_adjustment.skill_adjustment return quality_value @classmethod def setup_crafted_object(cls, crafted_object, crafter, is_final_product): pass @classproperty def crafting_price(cls): if cls._no_initial_charge: return 0 if isinstance(cls.crafting_cost, int): return cls.crafting_cost return int(cls._retail_price * cls.crafting_cost) @classproperty def retail_price(cls): return cls._retail_price @classproperty def simoleon_value_modifiers(cls): return cls.final_product.simoleon_value_modifiers_map @classproperty def simoleon_value_skill_curve(cls): return cls.final_product.simoleon_value_skill_curve @classproperty def masterworks_data(cls): return cls.final_product.masterworks @classproperty def required_skill_level(cls): if cls.skill_test is not None: return cls.skill_test.skill_range_min return 0 @classproperty def required_skill(cls): if cls.skill_test is not None: return cls.skill_test.skill @classmethod def get_base_recipe(cls): return cls.base_recipe or cls @classmethod def get_recipe_picker_name(cls, *args): if cls.multi_serving_name is not None: return cls.multi_serving_name(*args) return cls.name(*args) @classmethod def get_recipe_name(cls, *args): return cls.name(*args) @classmethod def get_price(cls, is_retail=False, ingredient_modifier=1): if is_retail: if cls.retail_price != 0: return cls.retail_price return cls.crafting_price return cls.crafting_price + int( cls.delta_ingredient_price * ingredient_modifier) @classmethod def calculate_autonomy_weight(cls, sim): total_weight = cls.autonomy_weight for (buff, weight) in cls.buff_weight_multipliers.items(): while sim.has_buff(buff): total_weight *= weight return total_weight @classproperty def total_visible_phases(cls): return len(cls._visible_phase_sequence) @classmethod def get_visible_phase_index(cls, phase): if phase in cls._visible_phase_sequence: return cls._visible_phase_sequence.index(phase) + 1 return 0 @classproperty def is_single_phase_recipe(cls): return len(cls._phases) == 1 @classmethod def update_hovertip(cls, owner, crafter=None): description = cls.recipe_description(crafter) genre = Genre.get_genre_localized_string(owner) if genre is not None: description = LocalizationHelperTuning.get_new_line_separated_strings( description, genre) value_text = cls.value_text if value_text: localized_value = value_text(owner.current_value) description = LocalizationHelperTuning.get_new_line_separated_strings( description, localized_value) objects.components.crafting_component._set_recipe_decription( owner, description) @property def debug_name(self): return type(self).__name__ @classmethod def debug_dump(cls, dump=dump_logger.warn): dump('Recipe Name: {}'.format(type(cls).__name__)) dump('Phases: {}'.format(len(cls.phases))) for phase in cls.phases.values(): dump(' Phase {}:'.format(phase.id)) while phase.num_turns > 0: dump(' Turns: {}'.format(phase.turns))