def __init__(self, start_description=None, end_description=None, **kwargs): super().__init__( key_type=TunableEnumEntry( description= '\n The weather type we are interested in.\n ', tunable_type=WeatherType, default=WeatherType.UNDEFINED), value_type=TunableTuple( start_loot=TunableList( description=start_description, tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)), end_loot=TunableList( description=end_description, tunable=LootActions. TunableReference( description= '\n The loot action applied.\n ', pack_safe=True))), **kwargs) self.cache_key = 'TunableWeatherAwareMapping'
class _PickerDisplaySnippet(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'display_snippet': TunableReference( description= '\n A display snippet that holds the display data that will\n populate the row in the picker.\n ', manager=services.get_instance_manager( sims4.resources.Types.SNIPPET), class_restrictions='DisplaySnippet', allow_none=False), 'loot_on_selected': TunableList( description= '\n A list of loot actions that will be applied to the subject Sim.\n ', tunable=LootActions.TunableReference( description= '\n A loot action applied to the subject Sim.\n ' )), 'tests': TunableTestSetWithTooltip( description= '\n Test set that must pass for this snippet to be available.\n NOTE: A tooltip test result will take priority over any\n instance display tooltip tuned in the display snippet.\n \n ID of the snippet will be the PickedItemID participant\n ' ), 'display_snippet_text_tokens': LocalizationTokens.TunableFactory( description= '\n Localization tokens passed into the display snippet text fields.\n These will be appended to the list of tokens when evaluating \n strings for this snippet. \n ', tuning_group=GroupNames.PICKERTUNING) } def test(self, resolver): return self.tests.run_tests(resolver, search_for_tooltip=True)
class ObjectRoutingBehaviorActionDestroyObjects(ObjectRoutingBehaviorAction): FACTORY_TUNABLES = { 'animation_success': OptionalTunable( description= '\n If enabled, the animation to play if there are objects to destroy.\n ', tunable=_ObjectRoutingActionAnimation.TunableFactory()), 'animation_failure': OptionalTunable( description= '\n If enabled, the animation to play if there are no objects to destroy.\n ', tunable=_ObjectRoutingActionAnimation.TunableFactory()), 'loot_success': TunableList( description= '\n For each destroyed object, apply this loot between the routing\n object (Actor) and the destroyed object (Object).\n ', tunable=LootActions.TunableReference()), 'object_selection_method': TunableVariant(tags=_DestroyObjectSelectionRuleTags.TunableFactory(), target_object=_DestroyObjectSelectionRuleTargetObject. TunableFactory(), default='tags') } def run_action_gen(self, timeline, obj, target): objects = self.object_selection_method.get_objects(obj, target) if not objects: if self.animation_failure is not None: result = yield from self.animation_failure( timeline, obj, target) return result yield return True yield def _callback(_): for o in objects: resolver = DoubleObjectResolver(obj, o) for loot_action in self.loot_success: loot_action.apply_to_resolver(resolver) o.remove_from_client(fade_duration=obj.FADE_DURATION) o.destroy( source=self, cause= 'Object being destroyed by ObjectRoutingBehaviorActionDestroyObjects' ) if self.animation_success is not None: result = yield from self.animation_success(timeline, obj, target, callback=_callback) if not result: return result yield else: _callback(timeline) return True yield
class SituationJobReward(HasTunableSingletonFactory, AutoFactoryInit): __qualname__ = 'SituationJobReward' FACTORY_TUNABLES = { 'reward': Reward.TunableReference(), 'loot': LootActions.TunableReference() } def apply(self, sim): if self.loot is not None: self.loot.apply_to_resolver(sim.get_resolver()) if self.reward is not None: self.reward.give_reward(sim.sim_info)
class SicknessTuning: SICKNESS_BUFFS_PLAYER_FACED = TunableList( description= "\n List of buffs that define if a sim is sick from what the player can \n see. The way sickness work, a sim might be sick but it may not be \n visible to the player, so on this list we should only tune the buff's\n that would make the sim sick on the players perspective.\n i.e. buffs that would make a child sim take a day of school.\n ", tunable=TunableReference(manager=services.buff_manager(), pack_safe=True)) LOOT_ACTIONS_ON_CHILD_CAREER_AUTO_SICK = TunableList( description= '\n Loot actions to test and apply on the event its time to go to work \n and the child sim is sick.\n i.e. notification... \n ', tunable=LootActions.TunableReference(pack_safe=True)) @classmethod def is_child_sim_sick(cls, sim_info): if not sim_info.is_child: return False return any( sim_info.has_buff(buff_type) for buff_type in SicknessTuning.SICKNESS_BUFFS_PLAYER_FACED)
class ScheduledLoot(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'loot': TunableList( description= '\n Loot applied when the effect is enacted.\n ', tunable=LootActions.TunableReference( description= '\n Loot applied when the effect is enacted.\n ', pack_safe=True)), 'schedule_data': WeeklySchedule.TunableFactory( description= '\n The information to schedule points during the week that\n the Street Policy Effect, if enacted, will award loot.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._loot_schedule = None self._resolver_gen = None def set_resolver_gen(self, resolver_gen): self._resolver_gen = resolver_gen def start_loot_schedule(self): if self._loot_schedule is not None: self._loot_schedule.destroy() self._loot_schedule = self.schedule_data( start_callback=self._handle_scheduled_loot_action, schedule_immediate=True) def stop_loot_schedule(self): self._loot_schedule.destroy() self._loot_schedule = None def _handle_scheduled_loot_action(self, scheduler, alarm_data, extra_data): if self._resolver_gen is None: return for resolver in self._resolver_gen(): for loot in self.loot: loot.apply_to_resolver(resolver)
class _NeighborWaitToBeGreetedState(CommonInteractionCompletedSituationState): FACTORY_TUNABLES = { 'early_exit_loot': TunableList( description= '\n A list of loot to apply between the neighbor and the active\n household Sims if this stiuation state times out.\n ', tunable=LootActions.TunableReference( description= '\n A loot action applied to all of the active household Sims if this\n situation state times out.\n ' )), 'early_exit_notification': TunableUiDialogNotificationSnippet( description= '\n Notification that will be shown when this situation state times\n out.\n ' ) } def __init__(self, *args, early_exit_loot=tuple(), early_exit_notification=None, **kwargs): super().__init__(*args, **kwargs) self._early_exit_loot = early_exit_loot self._early_exit_notification = early_exit_notification def _on_interaction_of_interest_complete(self, **kwargs): self._change_state(self.owner._hangout_state()) def timer_expired(self): for sim_info in services.active_household(): resolver = DoubleSimResolver(sim_info, self.owner._neighbor_sim.sim_info) for loot_action in self._early_exit_loot: loot_action.apply_to_resolver(resolver) resolver = DoubleSimResolver(services.active_sim_info(), self.owner._neighbor_sim.sim_info) early_exit_notification = self._early_exit_notification( services.active_sim_info(), resolver=resolver) early_exit_notification.show_dialog() self.owner._self_destruct()
class ObjectRoutingBehaviorActionApplyLoot(ObjectRoutingBehaviorAction): FACTORY_TUNABLES = { 'loot_actions': TunableList( description= "\n Loot to apply.\n Participant type 'Actor' refers to the object that is routing (ie, the 'bot').\n Participant type 'Object' refers to the target object the bot is acting upon.\n ", tunable=LootActions.TunableReference()) } def run_action_gen(self, timeline, obj, target): if self.loot_actions is None: return True yield if target is None: resolver = SingleObjectResolver(obj) else: resolver = DoubleObjectResolver(obj, target) for loot_action in self.loot_actions: loot_action.apply_to_resolver(resolver) return True yield
class DeliverBabyOnSurgeryTableInteraction(DeliverBabySuperInteraction): INSTANCE_TUNABLES = { 'bassinet_to_use': TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager()), 'bassinet_slot_type': SlotType.TunableReference( description= '\n SlotType used to place the bassinet when it is created.\n ' ), 'surgery_table_participant_type': TunableEnumEntry( description= '\n A reference to the ParticipantType that the surgery table will be\n in this interaction.\n ', tunable_type=ParticipantType, default=ParticipantType.Object), '_loot_per_baby': TunableList( description= '\n Loot that will be applied when a baby is born to a non NPC Sim. \n Actor will be the mom and TargetSim will be the sim_info of the baby.\n This will work for multiple babies as each loot will be applied to \n each baby sim info.\n \n None of these loots will be applied to a baby born to an NPC Sim.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'destroy_baby_in_bassinet_vfx': PlayEffect.TunableFactory( description= '\n The VFX to play when the baby is being destroyed during an NPC \n birth sequence.\n ' ), 'after_delivery_interaction': TunablePackSafeReference( description= '\n The interaction to push on the Sim after delivering a baby at the\n hospital.\n ', manager=services.affordance_manager(), allow_none=True) } FADE_BASSINET_EVENT_TAG = 201 DESTROY_BASSINET_EVENT_TAG = 202 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_exit_function(self._destroy_sim_info_if_neccesary) self._bassinet = None self._baby_object = None def _pre_perform(self, *args, **kwargs): bassinet_def = BabyTuning.get_corresponding_definition( self.bassinet_to_use) self._bassinet = create_object(bassinet_def) surgery_table = self.get_participant( self.surgery_table_participant_type) if surgery_table is None: logger.error('No surgery table found for {}', self, owner='tastle') return False surgery_table.part_owner.slot_object(self.bassinet_slot_type, self._bassinet) return True def _build_outcome_sequence(self): sequence = super()._build_outcome_sequence() return build_element( (self._handle_npc_bassinet_fade_and_effect, sequence)) def _handle_npc_bassinet_fade_and_effect(self, timeline): if self.sim.is_npc: def _fade_bassinet(_): if self._baby_object is not None: self._baby_object.fade_out() effect = self.destroy_baby_in_bassinet_vfx( self._baby_object) effect.start_one_shot() self.store_event_handler(_fade_bassinet, handler_id=self.FADE_BASSINET_EVENT_TAG) def _destroy_sim_info_if_neccesary(self): if self.sim.is_npc and self._baby_object is not None: baby_sim_info = self._baby_object.sim_info if self._parent_is_playable(baby_sim_info): baby_sim_info.inject_into_inactive_zone( baby_sim_info.household.home_zone_id) else: baby_sim_info.remove_permanently( household=baby_sim_info.household) def _parent_is_playable(self, sim_info): for parent_info in sim_info.genealogy.get_parent_sim_infos_gen(): if parent_info is not None: if parent_info.is_player_sim: return True return False def _complete_pregnancy_gen(self, timeline, pregnancy_tracker): offspring_data_list = list(pregnancy_tracker.get_offspring_data_gen()) new_baby = self._create_new_bassinet_with_baby(pregnancy_tracker, offspring_data_list[0], self._bassinet) self._bassinet.destroy() self._bassinet = None self._reset_actor_in_asms(new_baby, 'bassinet') self.interaction_parameters['created_target_id'] = new_baby.id self._baby_object = new_baby sim_infos = [] if not self.sim.is_npc or self._parent_is_playable(new_baby.sim_info): sim_infos = self._create_additional_babies( pregnancy_tracker, offspring_data_list[1:], position=new_baby.position, routing_surface=new_baby.routing_surface, create_bassinet=not self.sim.is_npc) sim_infos.append(new_baby.sim_info) if not self.sim.is_npc: self._apply_per_baby_loot(sim_infos) self._apply_inherited_loots(sim_infos, pregnancy_tracker) self._push_post_delivery_interaction(sim_infos) pregnancy_tracker.complete_pregnancy() return True yield def _push_post_delivery_interaction(self, sim_infos): if self._baby_object is not None and self.after_delivery_interaction is not None: def _destroy_all_bassinets(): for sim_info in sim_infos: bassinet_object = services.object_manager().get( sim_info.sim_id) if bassinet_object is not None: bassinet_object.make_transient() liabilities = (( CLEANUP_INTERACTION_CALLBACK_LIABILITY, CleanupInteractionCallbackLiability( cleanup_interaction_callback=_destroy_all_bassinets)), ) context = self.context.clone_for_continuation(self) self.sim.push_super_affordance(self.after_delivery_interaction, self._baby_object, context, liabilities=liabilities) def _apply_per_baby_loot(self, sim_infos): for sim_info in sim_infos: resolver = DoubleSimResolver(self.sim.sim_info, sim_info) for loot in self._loot_per_baby: loot.apply_to_resolver(resolver)
class BaseCivicPolicyProvider(ComponentContainer, HasStatisticComponent, HasTunableFactory, AutoFactoryInit): CIVIC_POLICY_SCHEDULE = TunableTuple(description='\n Global schedule to control when voting on civic policies is active.\n ', voting_open=TunableTimeOfWeek(description='\n The time of the week that voting for civic policies starts.\n ', default_day=Days.MONDAY, default_hour=8, default_minute=0), voting_close=TunableTimeOfWeek(description='\n The time of the week that voting for civic policies ends. Votes are\n tallied and policies are modified at this time.\n ', default_day=Days.SUNDAY, default_hour=16, default_minute=0), voting_close_warning_duration=TunableTimeSpan(description='\n Duration before the Voting Close to warn players that voting is about to close.\n ', default_hours=8), schedule_text=TunableLocalizedStringFactory(description='\n Text for the schedule string.\n ')) INFLUENCE_BUCK_TYPE = TunableEnumEntry(description='\n The type of Bucks used to hold Influence.\n ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True) INFLUENCE_TO_VOTE_COST = Tunable(description='\n The amount of influence used with 1 vote.\n ', tunable_type=int, default=10, export_modes=ExportModes.All) REPEAL_PETITION_THRESHOLD = Tunable(description='\n The number of petition signatures required to have a policy repealed.\n ', tunable_type=int, default=10, export_modes=ExportModes.All) COMMUNITY_BOARD_TAG = TunableTag(description='\n The tag of the community boards so we can find them in the world.\n ') VOTING_OPEN_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period opens.\n ') VOTING_OPEN_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period opens with maximum enabled policies.\n ') VOTING_CLOSE_WARNING_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting close approaches.\n ') VOTING_CLOSE_WARNING_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting close approaches with maximum enabled policies and\n a policy being repealed.\n ') VOTING_CLOSE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes.\n ') VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_SUCCESS = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes with maximum enabled\n policies and a policy being successfully repealed.\n ') VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_FAIL = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes with maximum enabled\n policies and a policy being unsuccessfully repealed.\n ') VOTING_CONTINUATION_AUTONOMY_COMMODITIES = TunableList(description='\n A list of static commodities that will be solved for by autonomy to\n find and push the vote interaction after viewing the community board.\n ', tunable=StaticCommodity.TunableReference(description='\n A static commodity that is solved for by autonomy to find the vote\n interaction to push. \n ', pack_safe=True)) COMMUNITY_BOARD_TEXT = TunableTuple(voting_closed_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible\n because voting is closed.\n "), voting_open_add_policy_tooltip_text=TunableLocalizedStringFactory(description="\n Text for the tooltip on the add policy button when it's disabled because\n voting is open. \n "), ineligible_voter_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible because\n the sim (first token) lives on a different street.\n "), ineligible_voter_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n Text for the tooltip on the confirm button when the button is disabled because\n the sim (first token) lives on a different street.\n '), no_room_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n Text for the tooltip on the confirm button when the button is disabled because\n already full up on enacted policies.\n '), no_room_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible because\n already full up on enacted policies.\n "), add_policy_picker=TunablePickerDialogVariant(description='\n The item picker dialog.\n ', available_picker_flags=ObjectPickerTuningFlags.ITEM)) CALL_TO_ACTIONS = TunableList(description='\n List of Call to Action that should be started to introduce the Civic Policy features.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.CALL_TO_ACTION), pack_safe=True)) FACTORY_TUNABLES = {'civic_policies': TunableSet(description='\n The civic policies that may be enacted.\n ', tunable=TunableReference(description='\n A civic policy.\n ', manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('BaseCivicPolicy',), pack_safe=True, export_modes=ExportModes.All)), 'voting_open_loot': TunableList(description='\n Loot applied to Resident Sims when voting opens.\n ', tunable=LootActions.TunableReference(description='\n Loot to apply on voting open.\n ', pack_safe=True), export_modes=ExportModes.ServerXML), 'voting_close_loot': TunableList(description='\n Loot applied to Resident Sims when voting opens.\n ', tunable=LootActions.TunableReference(description='\n Loot to apply on voting open.\n ', pack_safe=True), export_modes=ExportModes.ServerXML), 'community_board_dialog_title': TunableLocalizedStringFactoryVariant(description="\n The Community Board Dialog's title text.\n ", export_modes=ExportModes.ServerXML), 'initial_vote_test': TunableTestSet(description='\n If at least one test passes, and the user option is enabled, initial voting will\n be performed when voting opens.\n ', export_modes=ExportModes.ServerXML), 'daily_random_vote_test': TunableTestSet(description='\n If at least one test passes, and the user option is enabled, daily random voting\n will be performed at midnight.\n ', export_modes=ExportModes.ServerXML)} CIVIC_POLICY_TEST_EVENTS = (TestEvent.CivicPolicyOpenVoting, TestEvent.CivicPolicyDailyRandomVoting, TestEvent.CivicPolicyCloseVoting) def __init__(self, **kwargs): super().__init__(**kwargs) self.add_statistic_component() self._enacted_policies = set() self._balloted_policies = set() self._up_for_repeal_policies = set() self._civic_policies = set() for policy in self.civic_policies: self._civic_policies.add(policy(self)) @constproperty def is_sim(): return False @property def is_downloaded(self): return False def get_enacted_policies(self, tuning=False): if tuning: return set([type(p) for p in self._enacted_policies]) return self._enacted_policies def get_balloted_policies(self, tuning=False): if tuning: return set([type(p) for p in self._balloted_policies]) return self._balloted_policies def get_up_for_repeal_policies(self, tuning=False): if tuning: return set([type(p) for p in self._up_for_repeal_policies]) return self._up_for_repeal_policies def get_dormant_policies(self, tuning=False): policies = self._civic_policies - self._enacted_policies - self._balloted_policies - self._up_for_repeal_policies if tuning: return set([type(p) for p in policies]) return policies def get_civic_policies(self, tuning=False): if tuning: return self.civic_policies return self._civic_policies def new_enact_max_count(self): return min(len(self.civic_policies), self.max_enacted_policy_count) - len(self._enacted_policies) def _reset_voting_statistics(self): for policy in self._civic_policies: if policy.vote_count_statistic is None: logger.error('{} tuned without voting statistic', policy) else: self.set_stat_value(policy.vote_count_statistic, 0) self.commodity_tracker.check_for_unneeded_initial_statistics() self.statistic_tracker.check_for_unneeded_initial_statistics() def run_sim_voting_loot(self, loot_actions): for resolver in self.open_close_voting_loot_resolver_list: for loot in loot_actions: loot.apply_to_resolver(resolver) def run_auto_voting_tests(self, test_set): if not services.street_service().enable_automatic_voting: return False if not test_set: return True return test_set.run_tests(GlobalResolver()) def open_voting(self): self._reset_voting_statistics() self.finalize_startup() self.run_sim_voting_loot(self.voting_open_loot) if self.run_auto_voting_tests(self.initial_vote_test): for policy in self._balloted_policies: self.vote(policy, policy.get_initial_vote_count()) def close_voting(self): def get_most_voted_for_policy(policies): if not policies: return set() policy = max(policies, key=lambda policy: self.get_stat_value(policy.vote_count_statistic)) if self.get_stat_value(policy.vote_count_statistic) <= 0: return set() return set((policy,)) balloted_policies = self.get_balloted_policies() to_enact = get_most_voted_for_policy(balloted_policies) repealable_policies = self.get_up_for_repeal_policies() to_repeal = set() for policy in repealable_policies: if BaseCivicPolicyProvider.REPEAL_PETITION_THRESHOLD <= self.get_stat_value(policy.vote_count_statistic): to_repeal.add(policy) to_repeal -= to_enact to_enact -= self._enacted_policies self._enacted_policies -= to_repeal for policy in to_repeal: policy.repeal() to_enact_max_count = self.new_enact_max_count() while len(to_enact) > to_enact_max_count: to_enact.pop() self._enacted_policies.update(to_enact) for policy in to_enact: policy.enact() self._balloted_policies = set() self._up_for_repeal_policies = set() self._reset_voting_statistics() self.finalize_startup() self.run_sim_voting_loot(self.voting_close_loot) def get_schedule_text(self): return self.CIVIC_POLICY_SCHEDULE.schedule_text(self.CIVIC_POLICY_SCHEDULE.voting_open(), self.CIVIC_POLICY_SCHEDULE.voting_close()) def do_daily_vote(self): if self.run_auto_voting_tests(self.daily_random_vote_test): for policy in self._balloted_policies: self.vote(policy, policy.get_daily_vote_count()) @property def max_enacted_policy_count(self): raise NotImplementedError @property def max_balloted_policy_count(self): raise NotImplementedError @property def initial_balloted_policy_count(self): raise NotImplementedError @property def max_repealable_policy_count(self): raise NotImplementedError @property def open_close_voting_loot_resolver_list(self): raise NotImplementedError @classproperty def provider_type_id(cls): raise NotImplementedError def get_world_description_id(self): return 0 def is_eligible_voter(self, sim_info): raise NotImplementedError def is_new_policy_allowed(self, sim_info): return False def _select_balloted_policies(self): self._balloted_policies.clear() count_needed = self.initial_balloted_policy_count r = random.Random() dormant_policies = list(self.get_dormant_policies()) while dormant_policies: while len(self._balloted_policies) < count_needed: policy = r.choice(dormant_policies) dormant_policies.remove(policy) self._balloted_policies.add(policy) def finalize_startup(self): statistic_component = self.get_component(objects.components.types.STATISTIC_COMPONENT) statistic_component.on_finalize_load() if not self._civic_policies: return services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) services.get_event_manager().register(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) if not self._balloted_policies: self._select_balloted_policies() for policy in self._civic_policies: policy.finalize_startup() def stop_civic_policy_provider(self): services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) def get_policy_instance_for_tuning(self, policy_guid64): for inst in self._civic_policies: if policy_guid64 == inst.guid64: return inst def enact(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None or policy in self._enacted_policies: return False if self.new_enact_max_count() == 0: return False self._enacted_policies.add(policy) self._balloted_policies.discard(policy) self._up_for_repeal_policies.discard(policy) policy.enact() return True def repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None or policy not in self._enacted_policies: return False self._enacted_policies.discard(policy) self._up_for_repeal_policies.discard(policy) policy.repeal() return True def vote(self, policy, count=1, user_directed=False, lobby_interaction=False): policy_instance = self.get_policy_instance_for_tuning(policy.guid64) if policy_instance is None: return False return self.vote_by_instance(policy_instance, count, user_directed, lobby_interaction) def vote_by_instance(self, policy_instance, count=1, user_directed=False, lobby_interaction=False): if policy_instance.vote_count_statistic is not None: policy_list = None def get_current_rank(): policy_list.sort(key=lambda policy: (self.get_stat_value(policy.vote_count_statistic), policy.guid64), reverse=True) return policy_list.index(policy_instance) if user_directed: factor = 0 if policy_instance in self._balloted_policies: policy_list = list(self._balloted_policies) factor = 1 elif policy_instance in self._up_for_repeal_policies: policy_list = list(self._up_for_repeal_policies) factor = -1 orig_rank = get_current_rank() elif lobby_interaction: factor = 0 if policy_instance in self._balloted_policies: factor = 1 elif policy_instance in self._up_for_repeal_policies: factor = -1 value = self.get_stat_value(policy_instance.vote_count_statistic) + count self.set_stat_value(policy_instance.vote_count_statistic, value) services.street_service().update_community_board_tooltip(self) if user_directed: if policy_list is not None: with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_VOTE) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value) hook.write_guid(TELEMETRY_FIELD_PLAYER_VOTES, factor*count) hook.write_guid(TELEMETRY_FIELD_OLD_RANK, orig_rank) hook.write_guid(TELEMETRY_FIELD_NEW_RANK, get_current_rank()) if lobby_interaction: with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_LOBBY) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value) return True return False def _log_propose_telemetry(self, policy_instance, action): with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_PROPOSE) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_PROPOSE_ACTION, action) def add_to_ballot(self, policy_instance): if policy_instance.vote_count_statistic is not None and policy_instance not in self._balloted_policies: self._balloted_policies.add(policy_instance) self._log_propose_telemetry(policy_instance, TELEMETRY_FIELD_ACTION_VALUE_BALLOT) return True return False def add_for_repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None: return False if policy not in self._enacted_policies: return False if policy in self._up_for_repeal_policies: return False self._up_for_repeal_policies.add(policy) self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_REPEAL) return True def remove_from_repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None: return False if policy not in self._up_for_repeal_policies: return False self._up_for_repeal_policies.discard(policy) self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_CANCEL_REPEAL) return True def save(self, parent_data_msg): parent_data_msg.ClearField('policy_data') for policy in self._civic_policies: policy.save(parent_data_msg) parent_data_msg.ClearField('balloted_policy_ids') for policy in self._balloted_policies: parent_data_msg.balloted_policy_ids.append(policy.guid64) parent_data_msg.ClearField('up_for_repeal_policy_ids') for policy in self._up_for_repeal_policies: parent_data_msg.up_for_repeal_policy_ids.append(policy.guid64) parent_data_msg.ClearField('commodity_tracker') parent_data_msg.ClearField('statistics_tracker') parent_data_msg.ClearField('ranked_statistic_tracker') self.update_all_commodities() (commodites, _, ranked_statistics) = self.commodity_tracker.save() parent_data_msg.commodity_tracker.commodities.extend(commodites) regular_statistics = self.statistic_tracker.save() parent_data_msg.statistics_tracker.statistics.extend(regular_statistics) parent_data_msg.ranked_statistic_tracker.ranked_statistics.extend(ranked_statistics) def load(self, parent_data_msg): self.commodity_tracker.load(parent_data_msg.commodity_tracker.commodities) self.statistic_tracker.load(parent_data_msg.statistics_tracker.statistics) self.commodity_tracker.load(parent_data_msg.ranked_statistic_tracker.ranked_statistics) self._enacted_policies.clear() for policy_data in parent_data_msg.policy_data: policy = self.get_policy_instance_for_tuning(policy_data.policy_id) if policy: policy.load(policy_data) if policy.enacted: self._enacted_policies.add(policy) for policy_id in parent_data_msg.balloted_policy_ids: policy = self.get_policy_instance_for_tuning(policy_id) if policy: self._balloted_policies.add(policy) for policy_id in parent_data_msg.up_for_repeal_policy_ids: policy = self.get_policy_instance_for_tuning(policy_id) if policy: self._up_for_repeal_policies.add(policy) def handle_event(self, sim_info, event, resolver): if event == TestEvent.CivicPolicyDailyRandomVoting: self.do_daily_vote() elif event == TestEvent.CivicPolicyOpenVoting: self.open_voting() elif event == TestEvent.CivicPolicyCloseVoting: self.close_voting() def get_influence(self, sim_info): tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=False) if tracker is None: return 0 return tracker.get_bucks_amount_for_type(self.INFLUENCE_BUCK_TYPE) def modify_influence(self, sim_info, delta): if delta == 0: return tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=True) if tracker is None: return tracker.try_modify_bucks(self.INFLUENCE_BUCK_TYPE, delta) def populate_community_board_op(self, sim_info, op, target_id): op.sim_id = sim_info.id op.target_id = target_id op.influence_points = self.get_influence(sim_info) op.title = self.community_board_dialog_title() if hasattr(op, 'schedule_text'): op.schedule_text = self.get_schedule_text() for policy in self._enacted_policies: with ProtocolBufferRollback(op.enacted_policies) as enacted_policy: enacted_policy.policy_id = policy.guid64 if policy in self._up_for_repeal_policies: if policy.vote_count_statistic is None: enacted_policy.count = 0 else: enacted_policy.count = int(self.get_stat_value(policy.vote_count_statistic)) for policy in self._balloted_policies: with ProtocolBufferRollback(op.balloted_policies) as balloted_policy: balloted_policy.policy_id = policy.guid64 stat = policy.vote_count_statistic if stat is None: balloted_policy.count = 0 else: balloted_policy.count = int(self.get_stat_value(stat)) balloted_policy.max_count = stat.max_value op.provider_type = self.provider_type_id op.new_policy_allowed = self.is_new_policy_allowed(sim_info) if not services.street_service().voting_open: op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.voting_closed_policy_tooltip_text() if not self.is_eligible_voter(sim_info): op.disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_confirm_tooltip_text(sim_info) op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_policy_tooltip_text(sim_info) def _on_add_picker_selected(self, dialog): tag_objs = dialog.get_result_tags() if not tag_objs: return num_tags = len(tag_objs) can_add_more = dialog.max_selectable.number_selectable - num_tags > 0 if can_add_more: can_add_more = len(dialog.picker_rows) > num_tags op = CommunityBoardAddPolicy(tag_objs, dialog.target_sim.sim_id, can_add_more) Distributor.instance().add_op_with_no_owner(op) def create_add_policy_picker(self, sim_info, used_policy_ids): resolver = SingleSimResolver(sim_info) dialog = self.COMMUNITY_BOARD_TEXT.add_policy_picker(sim_info, resolver=resolver) for policy in self.get_dormant_policies(): if policy.guid64 not in used_policy_ids: tooltip = lambda *_, tooltip=policy.display_description: tooltip(sim_info) dialog.add_row(BasePickerRow(name=policy.display_name(sim_info), icon=policy.display_icon, tag=policy.guid64, row_tooltip=tooltip)) dialog.max_selectable.number_selectable = min(len(dialog.picker_rows), self.max_balloted_policy_count - len(self._balloted_policies) - len(used_policy_ids)) dialog.set_target_sim(sim_info) dialog.add_listener(self._on_add_picker_selected) dialog.show_dialog() def handle_vote_interaction(self, sim_info, target_id, push_continuation): sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is None: return target_object = services.object_manager().get(target_id) if not target_object: return for current_interaction in sim.si_state: interaction_target = current_interaction.target if interaction_target is None: continue if interaction_target.is_part: if interaction_target.part_owner is target_object: target_object = interaction_target break if interaction_target is target_object: break else: return if push_continuation: context = current_interaction.context.clone_for_continuation(current_interaction) autonomy_request = autonomy.autonomy_request.AutonomyRequest(sim, FullAutonomy, static_commodity_list=self.VOTING_CONTINUATION_AUTONOMY_COMMODITIES, object_list=(target_object,), insert_strategy=QueueInsertStrategy.NEXT, apply_opportunity_cost=False, is_script_request=True, ignore_user_directed_and_autonomous=True, context=context, si_state_view=sim.si_state, limited_autonomy_allowed=True, autonomy_mode_label_override='ParameterizedAutonomy', off_lot_autonomy_rule_override=UNLIMITED_AUTONOMY_RULE) autonomy_service = services.autonomy_service() results = autonomy_service.score_all_interactions(autonomy_request) chosen_interaction = autonomy_service.choose_best_interaction(results, autonomy_request) autonomy_request.invalidate_created_interactions(excluded_si=chosen_interaction) if chosen_interaction: target_affordance = current_interaction.generate_continuation_affordance(chosen_interaction.affordance) sim.push_super_affordance(target_affordance, target_object, context) current_interaction.cancel(FinishingType.NATURAL, 'Finished viewing board')
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 RankedStatistic( HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC) ): @classmethod def _verify_tuning_callback(cls): super()._verify_tuning_callback() ranks_tuned = [ level_data for level_data in cls.event_data.values() if level_data.rank_up ] ranks_needed = len(ranks_tuned) + 1 actual_ranks = len(cls.rank_tuning) tuned_rank_up_notifications = len(cls.rank_up_notification_tuning) tuned_rank_down_notifications = len(cls.rank_down_notification_tuning) if actual_ranks != ranks_needed: logger.error( '{} ranks have been enabled, but there is tuning for {} ranks in the rank_tuning. Please double check the tuning for {}', ranks_needed, actual_ranks, cls) if actual_ranks != tuned_rank_up_notifications: logger.error( 'There are {} ranks tuned but {} rank up notifications tuned. These need to be the same. Please double check the tuning for {}', actual_ranks, tuned_rank_up_notifications, cls) if tuned_rank_down_notifications > 0 and actual_ranks != tuned_rank_down_notifications: logger.error( 'There are {} ranks tuned but {} rank down notifications tuned. These need to be the same. Please double check the tuning for {}', actual_ranks, tuned_rank_down_notifications, cls) INSTANCE_TUNABLES = { 'stat_name': TunableLocalizedString( description= '\n Localized name of this statistic.\n ', allow_none=True), 'event_intervals': TunableList( description= '\n The level boundaries for an event, specified as a delta from the\n previous value.\n ', tunable=Tunable( description= '\n Points required to reach this level.\n ', tunable_type=int, default=0), export_modes=ExportModes.All), 'event_data': TunableMapping( description= '\n The data associated with a specific tuned event. \n \n The Key is the event number as tuned in the event intervals.\n \n The value is a list of loots to apply when the event occurs and an\n bool for whether or not to rank up the stat. \n ', key_type=int, value_type=TunableTuple( description= '\n The data associated with a tuned event from event_intervals.\n ', rank_up=Tunable( description= "\n If checked then this event will cause the statistic to rank\n up and all that entails. Currently that will increment\n the rank count.\n \n There should be a rank up entry for each of the levels \n tuned, except the initial rank. We assume that you don't \n need to rank into the initial rank. This means you will \n need one more level tuned than number of rank up events\n found in this list.\n ", tunable_type=bool, default=False), loot=TunableList( description= '\n A list of loots to apply when this event happens. This loot\n is only applied the first time you reach a specific level.\n If you want the loot applied every time you reach a level\n (for instance after you decay to a previous level and then\n regain a level) please use the loot_always tuning.\n ', tunable=TunableReference( description= '\n The loot to apply.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), tooltip=TunableLocalizedStringFactory( description= '\n The tooltip to display in the UI for each of the event\n lines. This is to be used for telling the user what loot \n they are going to get at an individual event.\n ' ), level_down_loot=TunableList( description= '\n A list of loots to apply when the Sim loses enough points \n to level down.\n ', tunable=LootActions.TunableReference(pack_safe=True)), tests=event_testing.tests.TunableTestSet( description= "\n Tests to run when reaching this level. If the tests don't \n pass then the value will be set back to min points for \n the rank before it. This means that the Sim won't be able\n to make any progress towards the rank with the failed\n tests.\n ", export_modes=ExportModes.ServerXML), loot_always=TunableList( description= '\n This loot is always awarded on level up, regardless of \n whether or not this level has already been achieved or not.\n \n If you want the loot to only be applied the first time you\n reach a certain level then please use the loot tuning.\n ', tunable=TunableReference( description= '\n The loot to award on level up.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), loot_always_on_load=TunableList( description= '\n This loot is always awarded when a sim loads with this\n level.\n ', tunable=LootActions.TunableReference(pack_safe=True)), export_class_name='EventDataTuple'), tuple_name='TunableEventData', export_modes=ExportModes.All), 'initial_rank': Tunable(description= '\n The initial rank for this stat.\n ', tunable_type=int, default=1, export_modes=ExportModes.All), 'rank_tuning': TunableMapping( description= '\n This is the tuning that is associated with a specific rank level \n instead of each individual event level. When the rank has increased \n the matching information will be retrieved from here and used.\n \n There needs to be an equal number of ranks tuned to match all of \n the rank up events in event data plus an extra one for the \n rank you start out on initially.\n ', key_type=int, value_type=TunableTuple( description= '\n A tuple of all the data for each Rank associated wit this\n ranked statistic.\n ', rank_name=TunableLocalizedString( description= "\n The rank's normal name.\n " ), icon=OptionalTunable( description= '\n If enabled then the Rank Statistic will have an icon \n associated with this Rank.\n ', tunable=TunableResourceKey( description= '\n Icon to be displayed for the rank.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), enabled_by_default=True), rank_description=OptionalTunable( description= '\n When enabled this string will be used as the description\n for the rank.\n ', tunable=TunableLocalizedString( description= "\n The rank's description.\n " )), rank_short_name=OptionalTunable( description= '\n When enabled this string will be used as an alternate \n short name for the rank.\n ', tunable=TunableLocalizedString( description= "\n The rank's short name.\n " )), hide_in_ui=Tunable( description= '\n If checked, this rank will not be shown in some places in the UI (XP bars, Relationship tooltip, Gallery)\n ', tunable_type=bool, default=False), export_class_name='RankDataTuple'), tuple_name='TunableRankData', export_modes=ExportModes.All), 'rank_down_notification_tuning': TunableMapping( description= '\n A mapping of Rank to tuning needed to display all the notifications\n when a Sim ranks down. \n \n The number of notifications tuned must match the number of items\n in rank_tuning.\n ', key_type=int, value_type=TunableTuple( description= '\n A Tuple containing both the rank down screen slam and the rank\n down notification to display.\n ', show_notification_tests=event_testing.tests.TunableTestSet( description= '\n Tests that must be true when the we want to show notification.\n ' ), rank_down_screen_slam=OptionalTunable( description= '\n Screen slam to show when Sim goes down to this rank level.\n Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n {1.String}, Rank Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), rank_down_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this\n rank. The text will be provided two tokens: the Sim owning\n the stat and a number representing the 1-based rank\n level.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })))), 'rank_up_notification_tuning': TunableMapping( description= '\n A mapping of Rank to tuning needed to display all the notifications\n when a Sim ranks up. \n \n The number of notifications tuned must match the number of items\n in rank_tuning.\n ', key_type=int, value_type=TunableTuple( description= '\n A Tuple containing both the rank up screen slam and the rank\n up notification to display.\n ', show_notification_tests=event_testing.tests.TunableTestSet( description= '\n Tests that must be true when the we want to show notification.\n ' ), rank_up_screen_slam=OptionalTunable( description= '\n Screen slam to show when reaches this rank level.\n Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n {1.String}, Rank Number - {2.Number}\n \n This will only happen the first time a rank is reached.\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), rank_up_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this\n rank. The text will be provided two tokens: the Sim owning\n the stat and a number representing the 1-based rank\n level.\n \n This will only happen the first time a rank is reached. If\n you want to show a display on subsequent rank ups you can \n tune the re_rank_up_notifcation.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })), re_rank_up_notification=OptionalTunable( description= '\n The notification to display when the Sim obtains this rank\n every time other than the first time. For instance if the\n Sim achieves rank 3, drops down to rank 2 because of decay,\n and then re-achieves rank 3, that is when this dialog will\n be displayed.\n \n If you want this dialog to be displayed the first time the\n Sim reaches a rank please tune rank_up_notification instead.\n ', tunable=UiDialogNotification.TunableFactory( locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'secondary_icon': None })))), 'tags': TunableList( description= '\n The associated categories of the ranked statistic.\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True)), 'icon': TunableIcon( description="\n The ranked stat's icon.\n ", allow_none=True), 'initial_loot': TunableList( description= '\n A list of loots to apply when the Sim first receives this ranked\n statistic.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'min_decay_per_highest_level_achieved': TunableMapping( description= '\n A mapping of highest level reached to the absolute minimum \n that this Ranked Stat is allowed to decay to in ranks.\n ', key_type=int, value_type=TunableRange( description= '\n The lowest level this stat can decay to based on the associated\n highest level reached.\n ', tunable_type=int, minimum=1, default=1)), 'associated_bucks_types': TunableList( description= '\n A list of bucks types that are associated with this ranked stat.\n These bucks types may have tuned data that is affected by ranking\n up/down.\n ', tunable=TunableEnumEntry( description= '\n A buck type that is associated with this ranked stat.\n ', tunable_type=BucksType, default=BucksType.INVALID), unique_entries=True, export_modes=ExportModes.All), 'zero_out_on_lock': Tunable( description= '\n If checked, when this ranked stat is locked it will zero out\n the value, highest_level, and bucks.\n ', tunable_type=bool, default=True), 'headline': OptionalTunable( description= '\n If enabled when this relationship track updates we will display\n a headline update to the UI.\n ', tunable=TunableReference( description= '\n The headline that we want to send down.\n ', manager=services.get_instance_manager( sims4.resources.Types.HEADLINE)), tuning_group=GroupNames.UI), 'send_stat_update_for_npcs': Tunable( description= "\n If checked then whenever we attempt to send the ranked stat update\n message it will be sent, even if the Sim is an NPC.\n \n NOTE: We don't want to mark very many of the stats like this. This \n is being done to make sure that Fame gets sent so we don't have\n to request Fame when building the tooltip for sims which could be\n really slow.\n ", tunable_type=bool, default=False), 'center_bar_tooltip': Tunable( description= '\n If true, always put motive panel ranked stat bar tooltip at the center.\n If false, put tooltip on each increment mark instead.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), 'visible': Tunable( description= '\n Whether or not statistic should be sent to client.\n \n NOTE: Please work with your UI engineering partner to determine if this \n should be True. If False, for performance reasons, \n the stat will be removed from the sim if their\n current value matches the default convergence value. \n ', tunable_type=bool, default=False, export_modes=ExportModes.All) } REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning') def __init__(self, tracker): self._rank_level = self.initial_rank self.highest_level = 0 super().__init__(tracker, self.initial_value) self._current_event_level = 0 self.previous_event_level = 0 self._notifications_disabled = False self._initial_loots_awarded = False self._suppress_level_up_telemetry = False @constproperty def is_ranked(): return True @property def rank_level(self): return self._rank_level @property def process_non_selectable_sim(self): return True @rank_level.setter def rank_level(self, value): self._rank_level = value services.get_event_manager().process_event( TestEvent.RankedStatisticChange, sim_info=self.tracker.owner.sim_info) @property def highest_rank_achieved(self): rank_level = self.initial_rank for i in range(1, self.highest_level + 1): if self.event_data.get(i).rank_up: rank_level += 1 return rank_level @property def is_visible(self): if self.tracker is None or not self.tracker.owner.is_sim: return False return self.visible @property def instance_required(self): if self.is_visible: return True return super().instance_required def increase_rank_level(self, new_rank=True, from_add=False): self.rank_level += 1 self._on_rank_up(new_rank=new_rank, from_add=from_add) def increase_rank_levels(self, levels): start_level = self.rank_level self.rank_level = start_level + levels self.send_rank_change_update_message(start_level, start_level + levels) def decrease_rank_level(self): self.rank_level = max(self.rank_level - 1, 0) self._on_rank_down() def _on_rank_up(self, new_rank=True, from_add=False): current_rank = self.rank_level self.send_rank_change_update_message(current_rank - 1, current_rank) sim_info = self.tracker.owner.sim_info rank_data = self.rank_tuning.get(current_rank) rank_up_data = self.rank_up_notification_tuning.get(current_rank) if rank_data is None: logger.error( 'Sim {}: {} is trying to rank up to level {} but there is no rank tuning.', sim_info, self, current_rank) return if not from_add and (sim_info.is_selectable and rank_up_data is not None ) and self.can_show_notification(rank_up_data): icon_override = None if rank_data.icon is None else IconInfoData( icon_resource=rank_data.icon) if new_rank: self._show_initial_rank_up_notifications( sim_info, current_rank, rank_data, rank_up_data, icon_override) else: self._show_re_rank_up_notifications(sim_info, current_rank, rank_data, rank_up_data, icon_override) def _show_initial_rank_up_notifications(self, sim_info, current_rank, rank_data, rank_up_data, icon_override): if rank_up_data.rank_up_notification is not None: notification = rank_up_data.rank_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog( icon_override=icon_override, secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(current_rank, )) if rank_up_data.rank_up_screen_slam is not None: rank_up_data.rank_up_screen_slam.send_screen_slam_message( sim_info, sim_info, rank_data.rank_name, current_rank) def _show_re_rank_up_notifications(self, sim_info, current_rank, rank_data, rank_up_data, icon_override): if rank_up_data.re_rank_up_notification is not None: notification = rank_up_data.re_rank_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog( icon_override=icon_override, secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(current_rank, )) def _on_rank_down(self): current_rank = self.rank_level self.send_rank_change_update_message(current_rank + 1, current_rank) sim_info = self.tracker.owner.sim_info rank_data = self.rank_tuning.get(current_rank) rank_down_data = self.rank_down_notification_tuning.get(current_rank) if rank_data is None: logger.error( 'Sim {}: {} is trying to rank down to level {} but there is no rank tuning.', sim_info, self, current_rank) return if self.can_show_notification(rank_down_data): if rank_down_data.rank_down_notification is not None: notification = rank_down_data.rank_down_notification( sim_info, resolver=SingleSimResolver(sim_info)) icon_override = None if rank_data.icon is None else IconInfoData( icon_resource=rank_data.icon) notification.show_dialog(icon_override=icon_override, secondary_icon_override=IconInfoData( obj_instance=sim_info), additional_tokens=(current_rank, )) if rank_down_data.rank_down_screen_slam is not None: rank_down_data.rank_down_screen_slam.send_screen_slam_message( sim_info, sim_info, rank_data.rank_name, current_rank) for bucks_type in self.associated_bucks_types: bucks_tracker = BucksUtils.get_tracker_for_bucks_type( bucks_type, owner_id=self.tracker.owner.id) bucks_tracker.validate_perks(bucks_type, self.rank_level) def on_add(self): super().on_add() self.tracker.owner.sim_info.on_add_ranked_statistic() self.on_stat_event(self.highest_level, self.get_user_value(), from_add=True) self.previous_event_level = self.get_user_value() if self.tracker.owner.is_simulating: self.apply_initial_loot() @classmethod def get_level_list(cls): return list(cls.event_intervals) def on_initial_startup(self): super().on_initial_startup() self.decay_enabled = self.tracker.owner.is_selectable and not self.tracker.owner.is_locked( self) @staticmethod def _callback_handler(stat_inst): new_level = stat_inst.get_user_value() stat_inst.on_stat_event(stat_inst.previous_event_level, new_level) stat_inst.previous_event_level = new_level stat_inst.refresh_threshold_callback() def on_stat_event(self, old_level, new_level, from_add=False): batch_rank_levels = 0 while old_level < new_level: old_level += 1 event_data = self.event_data.get(old_level) if event_data is not None: if self.tracker.owner.is_simulating: resolver = SingleSimResolver(self.tracker.owner) is_new_level = old_level > self.highest_level if is_new_level: for loot in event_data.loot: loot.apply_to_resolver(resolver) self.highest_level = old_level if event_data.rank_up: self.increase_rank_level(new_rank=is_new_level, from_add=from_add) for loot in event_data.loot_always: loot.apply_to_resolver(resolver) elif event_data.rank_up: batch_rank_levels += 1 if self.tracker.owner.is_npc: if not from_add: self._handle_level_up_telemetry(old_level) self._handle_level_up_telemetry(old_level) if batch_rank_levels > 0: self.increase_rank_levels(batch_rank_levels) else: self.create_and_send_commodity_update_msg(is_rate_change=False) @contextlib.contextmanager def suppress_level_up_telemetry(self): if self._suppress_level_up_telemetry: yield None else: self._suppress_level_up_telemetry = True try: yield None finally: self._suppress_level_up_telemetry = False def _handle_level_up_telemetry(self, level): if not self._suppress_level_up_telemetry: with telemetry_helper.begin_hook( ranked_stat_telemetry_writer, TELEMETRY_HOOK_RANKED_STAT_LEVEL_UP) as hook: hook.write_guid(TELEMETRY_FIELD_RANKED_STAT_TYPE, self.guid64) hook.write_int(TELEMETRY_FIELD_RANKED_STAT_LEVEL, level) @sims4.utils.classproperty def max_value(cls): return cls.get_max_skill_value() @sims4.utils.classproperty def min_value(cls): return 0 @sims4.utils.classproperty def best_value(cls): return cls.max_value @sims4.utils.classproperty def max_rank(cls): (_, rank) = cls.calculate_level_and_rank(cls.max_value) return rank @classmethod def convert_to_user_value(cls, value): if not cls.get_level_list(): return 0 current_value = value for (level, level_threshold) in enumerate(cls.get_level_list()): current_value -= level_threshold if current_value < 0: return level return level + 1 def can_show_notification(self, rank_data): if self._notifications_disabled: return False elif rank_data is not None and rank_data.show_notification_tests is not None: resolver = event_testing.resolver.SingleSimResolver( self.tracker.owner) result = rank_data.show_notification_tests.run_tests(resolver) if not result: return False return True def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_points = self.get_value() old_user_value = self.get_user_value() value_to_set = value if not from_load: value_to_set = self._get_valid_value(value, old_user_value) minimum_level = self._get_minimum_decay_level() value_to_set = max(value_to_set, minimum_level) super().set_value(value_to_set, *args, from_load=from_load, interaction=interaction, **kwargs) new_user_value = self.get_user_value() if not from_load: self._handle_level_down(old_user_value, new_user_value) sim_info = self._tracker._owner new_points = self.get_value() stat_type = self.stat_type if old_points == self.initial_value or old_points != new_points: services.get_event_manager().process_event( TestEvent.StatValueUpdate, sim_info=sim_info, statistic=stat_type, custom_keys=(stat_type, )) self.send_commodity_progress_msg(is_rate_change=False) self.send_change_update_message(value - old_points, from_load=from_load) self.previous_event_level = new_user_value self.refresh_threshold_callback() def _update_value(self): minimum_decay = self._get_minimum_decay_level() old_value = self._value old_user_value = self.convert_to_user_value(self._value) super()._update_value(minimum_decay_value=minimum_decay) new_value = self._value new_user_value = self.convert_to_user_value(self._value) self._handle_level_down(old_user_value, new_user_value) if old_user_value > new_user_value: self.previous_event_level = new_user_value self.refresh_threshold_callback() stat_type = self.stat_type if new_value > old_value: sim_info = self._tracker._owner if self._tracker is not None else None services.get_event_manager().process_event( TestEvent.StatValueUpdate, sim_info=sim_info, statistic=stat_type, custom_keys=(stat_type, )) def _get_minimum_decay_level(self): min_rank = self.min_decay_per_highest_level_achieved.get( self.highest_level, None) if min_rank is None: return 0 points = self.points_to_rank(min_rank) return points def _handle_level_down(self, old_value, new_value): while new_value < old_value: event_data = self.event_data.get(old_value) if event_data is not None: resolver = SingleSimResolver(self.tracker.owner) for loot in event_data.level_down_loot: loot.apply_to_resolver(resolver) if event_data.rank_up: self.decrease_rank_level() old_value -= 1 def get_next_rank_level(self): current_value = self.get_user_value() index = current_value + 1 if index > len(self.event_data): return current_value while not self.event_data[index].rank_up: if index == len(self.event_data): break index += 1 return index @constproperty def remove_on_convergence(): return False def send_commodity_progress_msg(self, is_rate_change=True): self.create_and_send_commodity_update_msg( is_rate_change=is_rate_change) @classmethod def points_to_level(cls, event_level): level = 0 running_sum = 0 level_list = cls.get_level_list() while level < len(level_list): while level < event_level: running_sum += level_list[level] level += 1 return running_sum @classmethod def points_to_rank(cls, rank_level): rank = cls.initial_rank level = 0 running_sum = 0 level_list = cls.get_level_list() while rank < rank_level: while level < len(level_list): event_data = cls.event_data.get(level) if event_data is not None: if cls.event_data[level].rank_up: rank += 1 if rank < rank_level: running_sum += level_list[level] level += 1 return running_sum def points_to_current_rank(self): return self.points_to_rank(self.rank_level) def create_and_send_commodity_update_msg(self, is_rate_change=True, allow_npc=False, from_add=False): ranked_stat_msg = Commodities_pb2.RankedStatisticProgressUpdate() ranked_stat_msg.stat_id = self.guid64 ranked_stat_msg.change_rate = self.get_change_rate() ranked_stat_msg.rank = self.rank_level difference = self.get_value() - self.points_to_current_rank() ranked_stat_msg.curr_rank_points = int( difference) if difference > 0 else 0 send_sim_ranked_stat_update_message(self.tracker.owner, ranked_stat_msg, allow_npc=allow_npc or self.send_stat_update_for_npcs) @classmethod def send_commodity_update_message(cls, sim_info, old_value, new_value): commodity_tracker = sim_info.commodity_tracker if commodity_tracker is None: return stat_instance = commodity_tracker.get_statistic(cls) if stat_instance is None: return stat_instance.create_and_send_commodity_update_msg(is_rate_change=True) def send_change_update_message(self, amount, from_load=False): if from_load: return if self.headline is None: return sim = self.tracker.owner if sim.is_selectable: self.headline.send_headline_message(sim, amount) def send_rank_change_update_message(self, previous_rank, current_rank): msg = Commodities_pb2.RankedStatisticRankChangedUpdate() msg.stat_id = self.guid64 msg.previous_rank = previous_rank msg.current_rank = current_rank send_sim_ranked_stat_change_rank_change_update_message( self.tracker.owner, msg) self.send_commodity_progress_msg() def on_sim_ready_to_simulate(self): level = self.get_user_value() event_data = self.event_data.get(level) if event_data is not None: resolver = SingleSimResolver(self.tracker.owner) for loot in event_data.loot_always_on_load: loot.apply_to_resolver(resolver) self.apply_initial_loot() def apply_initial_loot(self): if not self.initial_loot: return if self._initial_loots_awarded: return resolver = SingleSimResolver(self.tracker.owner) for loot in self.initial_loot: loot.apply_to_resolver(resolver) self._initial_loots_awarded = True def _get_valid_value(self, value, old_score): new_score = self.convert_to_user_value(value) if old_score <= new_score: resolver = SingleSimResolver(self.tracker.owner) while old_score <= new_score: old_score += 1 event_data = self.event_data.get(old_score) if event_data is not None: if not event_data.tests.run_tests(resolver=resolver): points = self.points_to_level(old_score - 1) return points return value def on_lock(self, action_on_lock): self._notifications_disabled = True should_zero_out = self.zero_out_on_lock or action_on_lock == StatisticLockAction.USE_MIN_VALUE_TUNING if should_zero_out: self.highest_level = 0 super().on_lock(action_on_lock) if should_zero_out: self.reset_bucks() self._notifications_disabled = False def reset_bucks(self): for bucks_type in self.associated_bucks_types: bucks_tracker = BucksUtils.get_tracker_for_bucks_type( bucks_type, self.tracker.owner.id) if bucks_tracker is not None: bucks_tracker.try_modify_bucks( bucks_type, -bucks_tracker.get_bucks_amount_for_type(bucks_type)) @classmethod def calculate_level_and_rank(cls, value): level = 0 rank = cls.initial_rank for points_to_next_level in cls.get_level_list(): value -= points_to_next_level if value < 0: break level += 1 level_data = cls.event_data.get(level) if level_data.rank_up: rank += 1 return (level, rank) def set_level_and_rank(self): (level, rank) = self.calculate_level_and_rank(self.get_value()) self.previous_event_level = level self.rank_level = rank def should_display_delayed_decay_warning(self): if self.highest_level == 0: return False return super().should_display_delayed_decay_warning() @classproperty def valid_for_stat_testing(cls): return True @classmethod def load_statistic_data(cls, tracker, data): super().load_statistic_data(tracker, data) stat = tracker.get_statistic(cls) if stat is not None: stat._initial_loots_awarded = data.initial_loots_awarded stat.set_level_and_rank() stat.highest_level = data.highest_level stat.load_time_of_last_value_change(data) stat.fixup_callbacks_during_load() def save_statistic(self, commodities, skills, ranked_statistics, tracker): message = protocols.RankedStatistic() message.name_hash = self.guid64 message.value = self.get_saved_value() message.highest_level = self.highest_level message.initial_loots_awarded = self._initial_loots_awarded if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks( ) ranked_statistics.append(message)
def __init__(self, *args, **kwargs): super().__init__(work_performance=TunableMultiplier.TunableFactory(description='\n Multiplier on the base full day work performance (tunable at\n CareerLevel -> Performance Metrics -> Base Performance).\n '), money=TunableMultiplier.TunableFactory(description='\n Multiplier on full day pay, determined by hourly wage (tunable\n at Career Level -> Simoleons Per Hour), multiplied by work day\n length (tunable at Career Level -> Work Scheduler), modified by\n any additional multipliers (e.g. tuning on Career Level ->\n Simolean Trait Bonus, Career Track -> Overmax, etc.).\n '), text=TunableLocalizedStringFactory(description='\n Text shown at end of event notification/dialog if the Sim\n finishes at this medal.\n \n 0 param - Sim in the career\n '), additional_loots=TunableList(description='\n Any additional loot needed on this medal payout. Currently, this\n is used to award additional drama nodes/dialogs on this level.\n ', tunable=LootActions.TunableReference(description='\n The loot action applied.\n ', pack_safe=True)), **kwargs)
class TempleTuning: TEMPLES = TunableMapping( description= '\n A Mapping of Temple Templates (house descriptions) and the data\n associated with each temple.\n ', key_name='Template House Description', key_type=TunableHouseDescription(pack_safe=True), value_name='Temple Data', value_type=TunableTuple( description= '\n The data associated with the mapped temple template.\n ', rooms=TunableMapping( description= '\n A mapping of room number to the room data. Room number 0 will be\n the entrance room to the temple, room 1 will be the first room\n that needs to be unlocked, and so on.\n ', key_name='Room Number', key_type=int, value_name='Room Data', value_type=TunableTempleRoomData(pack_safe=True)), enter_lot_loot=TunableSet( description= '\n Loot applied to Sims when they enter or spawn in to this Temple.\n \n NOTE: Exit Lot Loot is not guaranteed to be given. For example,\n if the Sim walks onto the lot, player switches to a different\n zone, then summons that Sim, that Sim will bypass getting the\n exit loot.\n ', tunable=LootActions.TunableReference(pack_safe=True)), exit_lot_loot=TunableSet( description= '\n Loot applied to Sims when they exit or spawn out of this Temple.\n \n NOTE: This loot is not guaranteed to be given after Enter Lot\n Loot. For example, if the Sim walks onto the lot, player\n switches to a different zone, then summons that Sim, that Sim\n will bypass getting the exit loot.\n ', tunable=LootActions.TunableReference(pack_safe=True)))) GATE_TAG = TunableTag( description= '\n The tag used to find the gate objects inside Temples.\n ', filter_prefixes=('func_temple', )) TRAP_TAG = TunableTag( description= '\n The tag used to identify traps inside temples. This will be used to find\n placeholder traps as well.\n ', filter_prefixes=('func_temple', )) CHEST_TAG = TunableTag( description= "\n The tag used to identify the final chest of a temple. If it's in the\n open state, the temple will be considered solved.\n ", filter_prefixes=('func_temple', )) CHEST_OPEN_STATE = TunablePackSafeReference( description= '\n The state that indicates the chest is open.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue') GATE_STATE = TunablePackSafeReference( description= '\n The state for temple gates. Used for easy look up.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState') GATE_UNLOCK_STATE = TunablePackSafeReference( description='\n The unlock state for temple gates.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue') TEMPLE_LOT_DESCRIPTION = TunablePackSafeLotDescription( description= '\n A reference to the lot description file for the temple lot. This is used\n for easier zone ID lookups.\n ' ) GATE_LOCK_LOOT = LockDoor.TunableFactory( description= '\n The LockDoor loot to run on the gates in the temple on load when they\n should be locked.\n ' ) GATE_UNLOCK_LOOT = UnlockDoor.TunableFactory( description= '\n The UnlockDoor loot to run on the gates when they should be unlocked.\n ' ) CHEST_OPEN_INTEARCTION = TunableInteractionOfInterest( description= '\n A reference to the open interaction for chests. This interaction will be\n listened for to determine temple completion.\n ' )
class AuditionDramaNode(BaseDramaNode): INSTANCE_TUNABLES = { 'gig': Gig.TunableReference( description='\n Gig this audition is for.\n ' ), 'audition_prep_time': TunableTimeSpan( description= '\n Amount of time between the seed of the potential audition node\n to the start of the audition time. \n ', default_hours=5), 'audition_prep_recommendation': TunableLocalizedStringFactory( description= '\n String that gives the player more information on how to succeed\n in this audition.\n ' ), 'audition_prep_icon': OptionalTunable( description= '\n If enabled, this icon will be displayed with the audition preparation.\n ', tunable=TunableIcon( description= '\n Icon for audition preparation.\n ' )), 'audition_outcomes': TunableList( description= '\n List of loot and multipliers which are for audition outcomes.\n ', tunable=TunableTuple( description= '\n The information needed to determine whether or not the sim passes\n or fails this audition. We cannot rely on the outcome of the \n interaction because we need to run this test on uninstantiated \n sims as well. This is similar to the fallback outcomes in \n interactions.\n ', loot_list=TunableList( description= '\n Loot applied if this outcome is chosen\n ', tunable=LootActions.TunableReference(pack_safe=True)), weight=TunableMultiplier.TunableFactory( description= '\n A tunable list of tests and multipliers to apply to the \n weight of the outcome.\n ' ), is_success=Tunable( description= '\n Whether or not this is considered a success outcome.\n ', tunable_type=bool, default=False))), 'audition_rabbit_hole': RabbitHole.TunableReference( description= '\n Data required to put sim in rabbit hole.\n ' ), 'skip_audition': OptionalTunable( description= '\n If enabled, we can skip auditions if sim passes tuned tests.\n ', tunable=TunableTuple( description= '\n Data related to whether or not this audition can be skipped.\n ', skip_audition_tests=TunableTestSet( description= '\n Test to see if sim can skip this audition.\n ' ), skipped_audition_loot=TunableList( description= '\n Loot applied if sim manages to skip audition\n ', tunable=LootActions.TunableReference(pack_safe=True)))), 'advance_notice_time': TunableTimeSpan( description= '\n The amount of time between the alert and the start of the event.\n ', default_hours=1, locked_args={ 'days': 0, 'minutes': 0 }), 'loot_on_schedule': TunableList( description= '\n Loot applied if the audition drama node is scheduled successfully.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'advance_notice_notification': TunableUiDialogNotificationSnippet( description= '\n The notification that is displayed at the advance notice time.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._calculated_audition_time = None self._calculated_gig_time = None self._rabbit_hole_id = None @classproperty def drama_node_type(cls): return DramaNodeType.AUDITION @property def _require_instanced_sim(self): return False @classproperty def persist_when_active(cls): return True def get_picker_schedule_time(self): return self._calculated_audition_time def create_picker_row(self, owner=None, **kwargs): now_time = services.game_clock_service().now() min_audition_time = now_time + self.audition_prep_time() possible_audition_times = self.get_final_times_based_on_schedule( self.min_and_max_times, anchor_time=min_audition_time, scheduled_time_only=True) audition_time = min_audition_time if possible_audition_times is not None: now = services.time_service().sim_now for possible_audition_time in possible_audition_times: if possible_audition_time[0] >= now: audition_time = possible_audition_time[0] break gig = self.gig time_till_gig = gig.get_time_until_next_possible_gig(audition_time) if time_till_gig is None: return gig_time = audition_time + time_till_gig if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests( SingleSimResolver(owner)): formatted_string = Career.GIG_PICKER_SKIPPED_AUDITION_LOCALIZATION_FORMAT( gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, gig_time, self.audition_prep_recommendation()) else: formatted_string = Career.GIG_PICKER_LOCALIZATION_FORMAT( gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, audition_time, gig_time, self.audition_prep_recommendation()) self._calculated_audition_time = audition_time self._calculated_gig_time = gig_time return gig.create_picker_row(formatted_string, owner) def schedule(self, resolver, specific_time=None, time_modifier=TimeSpan.ZERO): if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests( resolver): for loot in self.skip_audition.skipped_audition_loot: loot.apply_to_resolver(resolver) resolver.sim_info_to_test.career_tracker.set_gig( self.gig, self._calculated_gig_time) return False success = super().schedule(resolver, specific_time=specific_time, time_modifier=time_modifier) if success: services.calendar_service().mark_on_calendar( self, advance_notice_time=self.advance_notice_time()) self._send_career_ui_update(is_add=True) for loot in self.loot_on_schedule: loot.apply_to_resolver(resolver) return success def cleanup(self, from_service_stop=False): services.calendar_service().remove_on_calendar(self.uid) self._send_career_ui_update(is_add=False) rabbit_hole_service = services.get_rabbit_hole_service() if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole( self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): rabbit_hole_service.remove_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) super().cleanup(from_service_stop=from_service_stop) def resume(self): if self._rabbit_hole_id and not services.get_rabbit_hole_service( ).is_in_rabbit_hole(self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): services.drama_scheduler_service().complete_node(self.uid) def _run(self): rabbit_hole_service = services.get_rabbit_hole_service() self._rabbit_hole_id = rabbit_hole_service.put_sim_in_managed_rabbithole( self._receiver_sim_info, self.audition_rabbit_hole) if self._rabbit_hole_id is None: self._on_sim_return(canceled=True) rabbit_hole_service.set_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def _on_sim_return(self, canceled=False): receiver_sim_info = self._receiver_sim_info resolver = SingleSimResolver(receiver_sim_info) weights = [] failure_outcomes = [] for outcome in self.audition_outcomes: if canceled: if not outcome.is_success: failure_outcomes.append(outcome) weight = outcome.weight.get_multiplier(resolver) if weight > 0: weights.append((weight, outcome)) else: weight = outcome.weight.get_multiplier(resolver) if weight > 0: weights.append((weight, outcome)) if failure_outcomes: selected_outcome = random.choice(failure_outcomes) else: selected_outcome = sims4.random.weighted_random_item(weights) if not selected_outcome: logger.error( 'No valid outcome is tuned on this audition. Verify weights in audition_outcome for {}.', self.guid64) services.drama_scheduler_service().complete_node(self.uid) return if selected_outcome.is_success: receiver_sim_info.career_tracker.set_gig(self.gig, self._calculated_gig_time) for loot in selected_outcome.loot_list: loot.apply_to_resolver(resolver) services.drama_scheduler_service().complete_node(self.uid) def _save_custom_data(self, writer): if self._calculated_audition_time is not None: writer.write_uint64(AUDITION_TIME_TOKEN, self._calculated_audition_time) if self._calculated_gig_time is not None: writer.write_uint64(GIG_TIME_TOKEN, self._calculated_gig_time) if self._rabbit_hole_id is not None: writer.write_uint64(RABBIT_HOLE_ID_TOKEN, self._rabbit_hole_id) def _load_custom_data(self, reader): self._calculated_audition_time = DateAndTime( reader.read_uint64(AUDITION_TIME_TOKEN, None)) self._calculated_gig_time = DateAndTime( reader.read_uint64(GIG_TIME_TOKEN, None)) self._rabbit_hole_id = reader.read_uint64(RABBIT_HOLE_ID_TOKEN, None) rabbit_hole_service = services.get_rabbit_hole_service() if not self._rabbit_hole_id: rabbit_hole_service = services.get_rabbit_hole_service() self._rabbit_hole_id = services.get_rabbit_hole_service( ).get_rabbit_hole_id_by_type(self._receiver_sim_info.id, self.audition_rabbit_hole) if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole( self._receiver_sim_info.id, rabbit_hole_id=self._rabbit_hole_id): rabbit_hole_service.set_rabbit_hole_expiration_callback( self._receiver_sim_info.id, self._rabbit_hole_id, self._on_sim_return) self._send_career_ui_update() return True def _send_career_ui_update(self, is_add=True): audition_update_msg = DistributorOps_pb2.AuditionUpdate() if is_add: self.gig.build_gig_msg( audition_update_msg.audition_info, self._receiver_sim_info, gig_time=self._calculated_gig_time, audition_time=self._calculated_audition_time) op = GenericProtocolBufferOp(Operation.AUDITION_UPDATE, audition_update_msg) build_icon_info_msg( IconInfoData(icon_resource=self.audition_prep_icon), self.audition_prep_recommendation(), audition_update_msg.recommended_task) Distributor.instance().add_op(self._receiver_sim_info, op) def load(self, drama_node_proto, schedule_alarm=True): super_success = super().load(drama_node_proto, schedule_alarm=schedule_alarm) if not super_success: return False services.calendar_service().mark_on_calendar( self, advance_notice_time=self.advance_notice_time()) return True def on_calendar_alert_alarm(self): receiver_sim_info = self._receiver_sim_info resolver = SingleSimResolver(receiver_sim_info) dialog = self.advance_notice_notification(receiver_sim_info, resolver=resolver) dialog.show_dialog()
class ObjectRoutingComponent(FootprintToggleMixin, Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_ROUTING_COMPONENT): FACTORY_TUNABLES = {'routing_behavior_map': TunableMapping(description='\n A mapping of states to behavior. When the object enters a state, its\n corresponding routing behavior is started.\n ', key_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue'), value_type=OptionalTunable(tunable=ObjectRoutingBehavior.TunableReference(), enabled_by_default=True, enabled_name='Start_Behavior', disabled_name='Stop_All_Behavior', disabled_value=UNSET)), 'privacy_rules': OptionalTunable(description='\n If enabled, this object will care about privacy regions.\n ', tunable=TunableTuple(description='\n Privacy rules for this object.\n ', on_enter=TunableTuple(description='\n Tuning for when this object is considered a violator of\n privacy.\n ', loot_list=TunableList(description='\n A list of loot operations to apply when the object\n enters a privacy region.\n ', tunable=LootActions.TunableReference(pack_safe=True))))), 'tracking_category': TunableEnumEntry(description='\n Used to classify routing objects for the purpose of putting them\n into buckets for the object routing service to restrict the number\n of simultaneously-active objects.\n ', tunable_type=ObjectRoutingBehaviorTrackingCategory, default=ObjectRoutingBehaviorTrackingCategory.NONE)} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._running_behavior = None self._idle_element = None self._previous_parent_ref = None self._pending_running_behavior = None self._privacy_violations = WeakSet() @property def previous_parent(self): if self._previous_parent_ref is not None: return self._previous_parent_ref() def _setup(self): master_controller = services.get_master_controller() master_controller.add_sim(self.owner) if self.privacy_rules: privacy_service = services.privacy_service() privacy_service.add_vehicle_to_monitor(self.owner) self.owner.routing_component.on_sim_added() self.add_callbacks() def on_add(self, *_, **__): zone = services.current_zone() if not zone.is_zone_loading: self._setup() def on_finalize_load(self): self._setup() def on_remove(self): self.remove_callbacks() self.owner.routing_component.on_sim_removed() master_controller = services.get_master_controller() master_controller.remove_sim(self.owner) if self.privacy_rules: privacy_service = services.privacy_service() privacy_service.remove_vehicle_to_monitor(self.owner) def add_callbacks(self): if self.privacy_rules: self.owner.register_on_location_changed(self._check_privacy) self.register_routing_event_callbacks() def remove_callbacks(self): if self.owner.is_on_location_changed_callback_registered(self._check_privacy): self.owner.unregister_on_location_changed(self._check_privacy) self.unregister_routing_event_callbacks() def handle_privacy_violation(self, privacy): if not self.privacy_rules: return resolver = SingleObjectResolver(self.owner) loots = LootOperationList(resolver, self.privacy_rules.on_enter.loot_list) loots.apply_operations() if privacy not in self._privacy_violations: self._privacy_violations.add(privacy) def violates_privacy(self, privacy): if not self.privacy_rules: return False elif not privacy.vehicle_violates_privacy(self.owner): return False return True def _check_privacy(self, _, old_location, new_location): if not self.privacy_rules: return for privacy in services.privacy_service().privacy_instances: if not privacy.privacy_violators & PrivacyViolators.VEHICLES: continue new_violation = privacy not in self._privacy_violations violates_privacy = self.violates_privacy(privacy) if new_violation: if violates_privacy: self.handle_privacy_violation(privacy) if not violates_privacy: self._privacy_violations.discard(privacy) elif not violates_privacy: self._privacy_violations.discard(privacy) def on_state_changed(self, state, old_value, new_value, from_init): if new_value is old_value: return if new_value not in self.routing_behavior_map: return self._stop_runnning_behavior() routing_behavior_type = self.routing_behavior_map[new_value] if routing_behavior_type is UNSET: return routing_behavior = routing_behavior_type(self.owner) self._set_running_behavior(routing_behavior) self._cancel_idle_behavior() def on_location_changed(self, old_location): parent = self.owner.parent if parent is not None: self._previous_parent_ref = parent.ref() def component_reset(self, reset_reason): if self._running_behavior is not None: self._pending_running_behavior = type(self._running_behavior) self._running_behavior.trigger_hard_stop() self._set_running_behavior(None) services.get_master_controller().on_reset_sim(self.owner, reset_reason) def post_component_reset(self): if self._pending_running_behavior is not None: routing_behavior = self._pending_running_behavior(self.owner) self._set_running_behavior(routing_behavior) self._pending_running_behavior = None self._cancel_idle_behavior() def _cancel_idle_behavior(self): if self._idle_element is not None: self._idle_element.trigger_soft_stop() self._idle_element = None def _set_running_behavior(self, new_behavior): if new_behavior == self._running_behavior: return self._running_behavior = new_behavior if self.tracking_category and self.tracking_category is not ObjectRoutingBehaviorTrackingCategory.NONE: routing_service = services.get_object_routing_service() if routing_service: if new_behavior: routing_service.on_routing_start(self.owner, self.tracking_category, new_behavior) else: routing_service.on_routing_stop(self.owner, self.tracking_category) @componentmethod def get_idle_element(self): self._idle_element = soft_sleep_forever() return (self._idle_element, self._cancel_idle_behavior) @componentmethod def get_next_work(self): if self._running_behavior is None or self.owner.has_work_locks: return WorkRequest() work_request = WorkRequest(work_element=self._running_behavior, required_sims=(self.owner,)) return work_request @componentmethod def get_next_work_priority(self): return PriorityExtended.SubLow @componentmethod def on_requested_as_resource(self, other_work): if not any(resource.is_sim for resource in other_work.resources): return self.restart_running_behavior() def restart_running_behavior(self): routing_behavior_type = type(self._running_behavior) if self._running_behavior is not None else None self._stop_runnning_behavior() if routing_behavior_type is not None: routing_behavior = routing_behavior_type(self.owner) self._set_running_behavior(routing_behavior) def _stop_runnning_behavior(self): if self._running_behavior is not None: self._running_behavior.trigger_soft_stop() self._set_running_behavior(None) @componentmethod def get_participant(self, participant_type=ParticipantType.Actor, **kwargs): participants = self.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)) @componentmethod def get_participants(self, participant_type, **kwargs): if participant_type is ParticipantType.Actor: obj = self._running_behavior._obj if self._running_behavior else None return (obj,) elif participant_type is ParticipantType.Object: target = self._running_behavior.get_target() if self._running_behavior else None return (target,)
class ObjectDefinitionPickerInteraction(ObjectPickerInteraction): INSTANCE_TUNABLES = { 'object_tags_or_definition': TunableVariant( description= '\n The method that will be used to generate the list of objects that\n will populate the picker.\n ', all_items=DefinitionsFromTags.TunableFactory( description= '\n Look through all the items and populate any with these tags.\n \n This should be accompanied with specific filtering tags in\n Object Populate Filter to get a good result.\n ' ), specific_items=DefinitionsExplicit.TunableFactory( description= '\n A list of specific items that is populated in this\n dialog.\n ' ), tuning_group=GroupNames.PICKERTUNING), 'definition_to_loot': TunableList( description= '\n Does a definition test to see loot should be applied to subjects.\n ', tunable=TunableTuple( description= '\n Loot to apply if definition id passes.\n ', definition_id_test=DefinitionIdFilter.TunableFactory( description= '\n Definition to test for.\n ' ), loot_to_apply=LootActions.TunableReference( description= '\n Loot to apply.\n ' )), tuning_group=GroupNames.PICKERTUNING) } @classmethod def has_valid_choice(cls, target, context, **kwargs): return cls.object_tags_or_definition.has_choices(cls, target=target, context=context, sim=context.sim, **kwargs) @flexmethod def _get_objects_gen(cls, inst, *args, **kwargs): inst_or_cls = inst if inst is not None else cls yield from inst_or_cls.object_tags_or_definition.get_items_gen() @flexmethod def create_row(cls, inst, row_obj, context=DEFAULT, target=DEFAULT): inst_or_cls = inst if inst is not None else cls icon_info = IconInfoData(obj_def_id=row_obj.id, obj_geo_hash=row_obj.thumbnail_geo_state_hash, obj_material_hash=row_obj.material_variant) row = ObjectPickerRow( object_id=row_obj.id, def_id=row_obj.id, icon_info=icon_info, tag=row_obj, name=LocalizationHelperTuning.get_object_name(row_obj)) inst_or_cls._test_continuation(row, context=context, target=target) return row def on_choice_selected(self, choice_tag, **kwargs): if choice_tag is None: return resolver = self.get_resolver() for id_loot_test in self.definition_to_loot: if id_loot_test.definition_id_test(choice_tag): id_loot_test.loot_to_apply.apply_to_resolver(resolver) super().on_choice_selected(choice_tag, **kwargs)
class CareerTone(AwayAction): __qualname__ = 'CareerTone' INSTANCE_TUNABLES = { 'dominant_tone_loot_actions': TunableList( description= '\n Loot to apply at the end of a work period if this tone ran for the\n most amount of time out of all tones.\n ', tunable=LootActions.TunableReference()), 'performance_multiplier': Tunable( description= '\n Performance multiplier applied to work performance gain.\n ', tunable_type=float, default=1) } runtime_commodity = None @classmethod def _tuning_loaded_callback(cls): if cls.runtime_commodity is not None: return commodity = RuntimeCommodity.generate(cls.__name__) commodity.decay_rate = 0 commodity.convergence_value = 0 commodity.remove_on_convergence = True commodity.visible = False commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK commodity.min_value_tuning = 0 commodity.initial_value = 0 commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP cls.runtime_commodity = commodity def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._performance_change_alarm_handle = None self._last_performance_change_time = None def run(self, callback): super().run(callback) self._last_performance_change_time = services.time_service().sim_now time_span = clock.interval_in_sim_minutes( Career.CAREER_PERFORMANCE_UPDATE_INTERVAL) self._performance_change_alarm_handle = alarms.add_alarm( self, time_span, self._do_performance_change, repeating=True) def stop(self): if self._performance_change_alarm_handle is not None: alarms.cancel_alarm(self._performance_change_alarm_handle) self._performance_change_alarm_handle = None self._apply_performance_change() super().stop() def _do_performance_change(self, alarm_handle): self._apply_performance_change() def _apply_performance_change(self): career = self.sim_info.career_tracker.get_at_work_career() if career is None: logger.error( 'CareerTone {} trying to update performance when Sim {} not at work', self, self.sim_info, owner='tingyul') return now = services.time_service().sim_now elapsed = now - self._last_performance_change_time self._last_performance_change_time = now career.apply_performance_change(elapsed, self.performance_multiplier) career.resend_career_data() def apply_dominant_tone_loot(self): resolver = self.get_resolver() for loot in self.dominant_tone_loot_actions: loot.apply_to_resolver(resolver)
class SecurityBouncerSituation(TendObjectSituation): INSTANCE_TUNABLES = { '_loots_on_create': TunableList( description= '\n Loot Actions that will be applied to staffed object in this \n situation on situation create.\n ', tunable=LootActions.TunableReference(), tuning_group=GroupNames.SPECIAL_CASES), '_loots_on_set_sim_job': TunableList( description= '\n Loot Actions that will be applied to a sim and staffed object in \n this situation when the sim has been assigned a job.\n ', tunable=LootActions.TunableReference(), tuning_group=GroupNames.SPECIAL_CASES), '_loots_on_remove_sim_from_situation': TunableList( description= '\n Loot Actions that will be applied to a sim and staffed object in \n this situation when the sim is removed from situation.\n ', tunable=LootActions.TunableReference(), tuning_group=GroupNames.SPECIAL_CASES), '_loots_on_destroy': TunableList( description= '\n Loot Actions that will be applied to staff member and staffed \n object in this situation on situation destroy.\n ', tunable=LootActions.TunableReference(), tuning_group=GroupNames.SPECIAL_CASES) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._except_sim_ids = None self._persisted_sim_id = None self._load_sim_id_exception_lock_data() self._apply_loots(self._loots_on_create) def _on_set_sim_job(self, sim, job_type): super()._on_set_sim_job(sim, job_type) self._apply_loots(self._loots_on_set_sim_job) if self._persisted_sim_id is not None: if self._persisted_sim_id == sim.sim_id: self._apply_sim_id_exception_lock_data() self._persisted_sim_id = None def _on_remove_sim_from_situation(self, sim): if sim is self._staff_member: self._apply_loots(self._loots_on_remove_sim_from_situation) super()._on_remove_sim_from_situation(sim) def _destroy(self): self._apply_loots(self._loots_on_destroy) super()._destroy() def _apply_loots(self, loots): sim_info = None if self._staff_member is None else self._staff_member.sim_info obj = self._staffed_object if sim_info is not None: if obj is not None: resolver = SingleActorAndObjectResolver(sim_info, obj, self) else: resolver = SingleSimResolver(sim_info) elif obj is not None: resolver = SingleObjectResolver(obj) else: return for loot_action in loots: loot_action.apply_to_resolver(resolver) def load_situation(self): persisted_guest_info = next( iter(self._guest_list.get_persisted_sim_guest_infos()), None) if persisted_guest_info is not None: self._persisted_sim_id = persisted_guest_info.sim_id return super().load_situation() def _load_sim_id_exception_lock_data(self): reader = self._seed.custom_init_params_reader if reader is None: return self._except_sim_ids = reader.read_uint64s(EXCEPTION_SIM_ID_LIST_TOKEN, None) def _apply_sim_id_exception_lock_data(self): if self._staffed_object is None: return if not self._except_sim_ids: return lock_data = LockAllWithSimIdExceptionData( lock_priority=LockPriority.PLAYER_LOCK, lock_sides=LockSide.LOCK_FRONT, should_persist=False, except_actor=False, except_household=False) lock_data.except_sim_ids = self._except_sim_ids self._staffed_object.add_lock_data( lock_data, clear_existing_locks=ClearLock.CLEAR_NONE) def _save_sim_id_exception_lock_data(self, writer): if self._staffed_object is None: return portal_locking_component = self._staffed_object.portal_locking_component if portal_locking_component is None: return lock_datas = portal_locking_component.lock_datas if LockType.LOCK_ALL_WITH_SIMID_EXCEPTION not in lock_datas: return except_sim_ids = lock_datas[ LockType.LOCK_ALL_WITH_SIMID_EXCEPTION].except_sim_ids writer.write_uint64s(EXCEPTION_SIM_ID_LIST_TOKEN, except_sim_ids) def _save_custom_situation(self, writer): super()._save_custom_situation(writer) self._save_sim_id_exception_lock_data(writer)
class TutorialDramaNode(SituationDramaNode): INSTANCE_TUNABLES = {'weather_to_force': WeatherEvent.TunablePackSafeReference(description='\n The weather that will exist for the duration of this drama node.\n '), 'friendship_value': TunableRange(description='\n The value to set the friendship between player sim and housemate.\n ', tunable_type=int, maximum=100, minimum=-100, default=20), 'romance_value': TunableRange(description='\n The value to set the romance between player sim and housemate.\n ', tunable_type=int, maximum=100, minimum=-100, default=0), 'end_loots': TunableList(description='\n Loots to apply when the tutorial drama node ends.\n \n Player sim is Actor.\n Housemate sim is Targetsim.\n ', tunable=LootActions.TunableReference(pack_safe=True))} @classproperty def drama_node_type(cls): return DramaNodeType.TUTORIAL @classproperty def spawn_sims_during_zone_spin_up(cls): return True @classproperty def persist_when_active(cls): return True @property def _require_instanced_sim(self): return False def _run(self): super()._run() self._disable_non_tutorial_functionality() return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def get_player_sim_info(self): return self._receiver_sim_info def get_housemate_sim_info(self): return self._sender_sim_info def resume(self): super().resume() self._disable_non_tutorial_functionality() def end(self): drama_service = services.drama_scheduler_service() drama_service.set_enabled_state(True) if self._receiver_sim_info is not None: household = self._receiver_sim_info.household if household is not None: household.bills_manager.autopay_bills = False if self._sender_sim_info is not None: sim_info = self._sender_sim_info household = self._receiver_sim_info.household if sim_info in household: client = services.client_manager().get_first_client() if not client.set_active_sim_info(self._receiver_sim_info): logger.error('Tutorial Drama node ended without being able to set player sim to active.') client.remove_selectable_sim_info(sim_info) household.remove_sim_info(sim_info) sim_info.transfer_to_hidden_household() relationship = services.relationship_service().create_relationship(self._receiver_sim_info.sim_id, sim_info.sim_id) if relationship is not None: relationship.relationship_track_tracker.set_longterm_tracks_locked(False) npc_hosted_situation_service = services.npc_hosted_situation_service() if npc_hosted_situation_service is not None: npc_hosted_situation_service.resume_welcome_wagon() situation_manager = services.get_zone_situation_manager() if situation_manager is not None: situation = situation_manager.get_situation_by_type(self.situation_to_run) if situation is not None: situation_manager.destroy_situation_by_id(situation.id) weather_service = services.weather_service() if weather_service is not None: weather_service.reset_forecasts() fire_service = services.fire_service if fire_service is not None: fire_service.fire_enabled = True resolver = DoubleSimResolver(self._receiver_sim_info, self._sender_sim_info) for loot_action in self.end_loots: loot_action.apply_to_resolver(resolver) drama_service.complete_node(self.uid) def _disable_non_tutorial_functionality(self): services.drama_scheduler_service().set_enabled_state(False) npc_hosted_situation_service = services.npc_hosted_situation_service() if npc_hosted_situation_service is not None: npc_hosted_situation_service.suspend_welcome_wagon() if self._receiver_sim_info is not None: household = self._receiver_sim_info.household if household is not None: household.bills_manager.autopay_bills = True if self._sender_sim_info is not None: if self._receiver_sim_info.age == self._sender_sim_info.age: receiver_age_progress = self._receiver_sim_info.age_progress if receiver_age_progress <= self._sender_sim_info.age_progress: if receiver_age_progress == 0: self._receiver_sim_info.age_progress = 0.1 else: self._sender_sim_info.age_progress = 0 relationship_tracker = self._receiver_sim_info.relationship_tracker sender_id = self._sender_sim_info.sim_id relationship = services.relationship_service().create_relationship(self._receiver_sim_info.sim_id, sender_id) relationship_tracker.add_relationship_score(sender_id, self.friendship_value, RelationshipTrack.FRIENDSHIP_TRACK) relationship_tracker.add_relationship_score(sender_id, self.romance_value, RelationshipTrack.ROMANCE_TRACK) relationship.relationship_track_tracker.set_longterm_tracks_locked(True) weather_service = services.weather_service() if weather_service is not None: weather_service.force_start_weather_event(self.weather_to_force, None) weather_service.update_weather_type(during_load=True) fire_service = services.fire_service if fire_service is not None: fire_service.fire_enabled = False
class DeliverBabySuperInteraction(SuperInteraction, NameOffspringSuperInteractionMixin): INSTANCE_TUNABLES = { 'inherited_loots': OptionalTunable( description= '\n If enabled, these loots will be applied to the baby if the parents\n passed the test.\n ', tunable=TunableList( description= '\n List of loot given to the child based on the tests on the parents.\n ', tunable=TunableTuple( description= '\n Tuple of tests on parents to loots given to children.\n ', birther_test=TunableTestSet( description= '\n Test to run on the sim giving birth. \n ' ), non_birther_test=TunableTestSet( description= '\n Test to run on the sim not giving birth. \n ' ), child_loot=TunableList( description= '\n A list of loots to apply when both parents pass their tests.\n Actor = birther sim\n Target = baby\n ', tunable=LootActions.TunableReference( pack_safe=True))))) } def _get_name_dialog(self): data = PregnancyTuning.get_pregnancy_data(self.sim) return data.dialog(self.sim, resolver=SingleSimResolver(self.sim)) def _build_outcome_sequence(self, *args, **kwargs): sequence = super()._build_outcome_sequence(*args, **kwargs) pregnancy_tracker = self.sim.sim_info.pregnancy_tracker return element_utils.must_run( element_utils.build_critical_section_with_finally( self._name_and_create_babies_gen, sequence, lambda _: pregnancy_tracker.clear_pregnancy())) def _name_and_create_babies_gen(self, timeline): pregnancy_tracker = self.sim.sim_info.pregnancy_tracker if not pregnancy_tracker.is_pregnant: return False yield pregnancy_tracker.create_offspring_data() if not self.sim.is_npc: result = yield from self._do_renames_gen( timeline, list(pregnancy_tracker.get_offspring_data_gen())) if not result: return result yield else: pregnancy_tracker.assign_random_first_names_to_offspring_data() result = yield from self._complete_pregnancy_gen( timeline, pregnancy_tracker) return result yield def _reset_actor_in_asms(self, target, actor_name): animation_context = self.animation_context if animation_context is not None: for asm in animation_context.get_asms_gen(): asm.set_actor(actor_name, None) asm.set_actor(actor_name, target) def _create_new_bassinet_with_baby(self, pregnancy_tracker, offspring_data, empty_bassinet): sim_info = pregnancy_tracker.create_sim_info(offspring_data) new_bassinet = set_baby_sim_info_with_switch_id( empty_bassinet, sim_info) return new_bassinet def _create_additional_babies(self, pregnancy_tracker, extra_baby_list, position=None, routing_surface=None, create_bassinet=True): created_sim_infos = [] for offspring_data in extra_baby_list: sim_info = pregnancy_tracker.create_sim_info(offspring_data) created_sim_infos.append(sim_info) if not assign_bassinet_for_baby(sim_info): if create_bassinet: create_and_place_baby(sim_info, position=position, routing_surface=routing_surface) return created_sim_infos def _create_non_baby_offspring(self, pregnancy_tracker, offspring_data_list, target): created_sim_infos = [] for offspring_data in offspring_data_list: sim_info = pregnancy_tracker.create_sim_info(offspring_data) created_sim_infos.append(sim_info) SimSpawner.spawn_sim(sim_info, target.position, sim_location=target.location) return tuple(created_sim_infos) def _handle_show_baby_in_bassinet(self, animation_context, bassinet): if animation_context is not None: def on_show_baby(_): bassinet.enable_baby_state() bassinet.empty_baby_state() self.store_event_handler(on_show_baby, handler_id=100) def _handle_hide_pregnant_sims_belly(self, animation_context, pregnancy_tracker): if animation_context is not None: def on_hide_belly(_): pregnancy_tracker.clear_pregnancy_visuals() self.store_event_handler(on_hide_belly, handler_id=101) def _complete_pregnancy_gen(self, timeline, pregnancy_tracker): offspring_data_list = list(pregnancy_tracker.get_offspring_data_gen()) pregnant_sim = pregnancy_tracker._sim_info is_bassinet_birth = pregnant_sim.get_birth_age() == Age.BABY if is_bassinet_birth: new_target = self._create_new_bassinet_with_baby( pregnancy_tracker, offspring_data_list[0], self.target) self._reset_actor_in_asms(new_target, 'bassinet') self.target.transient = True self.set_target(new_target) self._handle_show_baby_in_bassinet(self.animation_context, new_target) sim_infos = self._create_additional_babies(pregnancy_tracker, offspring_data_list[1:]) sim_infos.append(new_target.sim_info) else: sim_infos = self._create_non_baby_offspring( pregnancy_tracker, offspring_data_list, self.sim) self._handle_hide_pregnant_sims_belly(self.animation_context, pregnancy_tracker) if not pregnant_sim.is_npc and self.inherited_loots: self._apply_inherited_loots(sim_infos, pregnancy_tracker) pregnancy_tracker.complete_pregnancy() return True yield def _apply_inherited_loots(self, sim_infos, pregnancy_tracker): inherited_loots = self.inherited_loots if not inherited_loots: return (birther, non_birther) = pregnancy_tracker.get_parents() for inherited_loot in inherited_loots: if birther and not inherited_loot.birther_test.run_tests( SingleSimResolver(birther)): continue if non_birther and not inherited_loot.non_birther_test.run_tests( SingleSimResolver(non_birther)): continue for sim_info in sim_infos: resolver = DoubleSimResolver(self.sim.sim_info, sim_info) for individual_loot in inherited_loot.child_loot: individual_loot.apply_to_resolver(resolver)
class UniversityMajor(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR)): INSTANCE_TUNABLES = {'courses': TunableList(description='\n List of courses, in order, for this major\n ', tunable=UniversityCourseData.TunableReference(), minlength=1), 'acceptance_score': TunableTuple(description='\n Score requirement to be accepted in this major as prestige degree.\n ', score=TunableMultiplier.TunableFactory(description='\n Define the base score and multiplier to calculate acceptance\n score of a Sim.\n '), score_threshold=TunableThreshold(description='\n The threshold to perform against the score to see if a Sim \n can be accepted in this major.\n ')), 'display_name': TunableLocalizedString(description="\n The major's name.\n ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'display_description': TunableLocalizedString(description="\n The major's description.\n ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icons': TunableTuple(description='\n Display icons for this major.\n ', icon=TunableIcon(description="\n The major's icon.\n "), icon_prestige=TunableIcon(description="\n The major's prestige icon.\n "), icon_high_res=TunableIcon(description="\n The major's high resolution icon.\n "), icon_prestige_high_res=TunableIcon(description="\n The major's prestige high resolution icon.\n "), export_class_name='UniversityMajorIconTuple', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'major_benefit_map': TunableMapping(description='\n University specific major benefit description. Each university can \n have its own description defined for this University Major.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableLocalizedString(description='\n Major benefit description.\n '), tuple_name='UniversityMajorBenefitMapping', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'graduation_reward': TunableMapping(description='\n Loot on graduation at each university for each GPA threshold\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableList(description='\n Loot for each GPA range (lower bound inclusive, upper bound\n exclusive.\n ', tunable=TunableTuple(gpa_range=TunableInterval(description='\n GPA range to receive this loot.\n Lower bound inclusive, upper bound exclusive.\n ', tunable_type=float, default_lower=0, default_upper=10), loot=TunableList(tunable=LootActions.TunableReference(description='\n The loot action applied.\n ', pack_safe=True))))), 'career_tracks': TunableList(description='\n List of career tracks for which the UI will indicate this major\n will provide benefit. Is not used to actually provide said benefit.\n ', tunable=TunableReference(description='\n These are the career tracks that will benefit from this major.\n ', manager=services.get_instance_manager(sims4.resources.Types.CAREER_TRACK), pack_safe=True), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary, unique_entries=True)} @classmethod def graduate(cls, sim_info, university, gpa): resolver = SingleSimResolver(sim_info) if university in cls.graduation_reward: for grad_reward in cls.graduation_reward[university]: if grad_reward.gpa_range.lower_bound <= gpa < grad_reward.gpa_range.upper_bound: for loot_action in grad_reward.loot: loot_action.apply_to_resolver(resolver) @classmethod def get_sim_acceptance_score(cls, sim_info): resolver = SingleSimResolver(sim_info) return cls.acceptance_score.score.get_multiplier(resolver) @classmethod def can_sim_be_accepted(cls, sim_info): sim_score = cls.get_sim_acceptance_score(sim_info) return cls.acceptance_score.score_threshold.compare(sim_score)
class ScarecrowSituation(ObjectBoundSituationMixin, SituationComplexCommon): INSTANCE_TUNABLES = { '_situation_job': SituationJob.TunableReference( description= '\n The situation job for the Sim.\n \n This job should define a spawn affordance that will trigger\n a continuation targeting the object the Sim spawns at.\n ', tuning_group=GroupNames.SITUATION), '_do_stuff_state': _DoStuffState.TunableFactory( description= '\n The state for the Sim doing stuff.\n \n This is the initial state after the Sim spawns onto the lot.\n\n Any on-activate affordances run in this role will target\n the object the Sim spawned near.\n ', display_name='1. Do Stuff', tuning_group=GroupNames.STATE), '_leave_state': _LeaveState.TunableFactory( description= '\n The state for the Sim leaving.\n \n Any on-activate affordances run in this role will target\n the object the Sim spawned near.\n ', display_name='2. Leave', tuning_group=GroupNames.STATE), '_spawn_object_targeting_affordance': TunableInteractionOfInterest( description= "\n Affordance that runs targeting the object that the object that the\n Sim had spawned at. This allows the situation to 'remember' that\n object and when that object is destroyed, the situation will\n be destroyed as well. \n ", tuning_group=GroupNames.SITUATION), '_spawn_object_reset_loots': LootActions.TunableReference( description= '\n Loots used to reset the object from which the scarecrow spawned from,\n to handle cases for when the scarecrow Sim is not on lot during load.\n ', tuning_group=GroupNames.SITUATION) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES @classmethod def _states(cls): return (SituationStateData( 1, _DoStuffState, partial(cls._do_stuff_state, situation_job=cls._situation_job)), SituationStateData( 2, _LeaveState, partial(cls._leave_state, situation_job=cls._situation_job))) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls._situation_job, None)] @classmethod def default_job(cls): return cls._situation_job @classmethod def get_sims_expected_to_be_in_situation(cls): return 1 def _get_role_state_overrides(self, sim, job_type, role_state_type, role_affordance_target): return (role_state_type, services.object_manager().get(self.bound_object_id)) def start_situation(self): super().start_situation() for custom_key in self._spawn_object_targeting_affordance.custom_keys_gen( ): self._register_test_event(TestEvent.InteractionStart, custom_key) self._change_state( self._do_stuff_state(situation_job=self._situation_job)) def load_situation(self): scarecrow_guest_info = next( iter(self._guest_list.get_persisted_sim_guest_infos())) if scarecrow_guest_info is None: self._reset_scarecrow_object() return False scarecrow_sim_info = services.sim_info_manager().get( scarecrow_guest_info.sim_id) if scarecrow_sim_info is None or scarecrow_sim_info.zone_id != services.current_zone_id( ): self._reset_scarecrow_object() return False return super().load_situation() def _reset_scarecrow_object(self): scarecrow_object = services.object_manager().get(self._bound_object_id) resolver = SingleObjectResolver(scarecrow_object) self._spawn_object_reset_loots.apply_to_resolver(resolver) def handle_event(self, sim_info, event, resolver): if event == TestEvent.InteractionStart: if self.is_sim_info_in_situation(sim_info) and resolver( self._spawn_object_targeting_affordance): target = resolver.get_participant(ParticipantType.Object) if target is None: logger.error( '{}: {} target is None, cannot find the object for this situation to bind to!', self, resolver) self._self_destruct() return self.bind_object(target) else: super().handle_event(sim_info, event, resolver) def go_to_leave_state(self): self._change_state( self._leave_state(situation_job=self._situation_job)) def _gsi_additional_data_gen(self): if isinstance(self._cur_state, _DoStuffState): yield ('Time till Leave State', str(self._cur_state.get_time_remaining()))
class CareerTone(AwayAction): INSTANCE_TUNABLES = { 'dominant_tone_loot_actions': TunableList( description= '\n Loot to apply at the end of a work period if this tone ran for the\n most amount of time out of all tones.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'))), 'performance_multiplier': Tunable( description= '\n Performance multiplier applied to work performance gain.\n ', tunable_type=float, default=1), 'periodic_sim_filter_loot': TunableList( description= '\n Loot to apply periodically to between the working Sim and other\n Sims, specified via a Sim filter.\n \n Example Usages:\n -Gain relationship with 2 coworkers every hour.\n -Every hour, there is a 5% chance of meeting a new coworker.\n ', tunable=TunableTuple( chance=SuccessChance.TunableFactory( description= '\n Chance per hour of loot being applied.\n ' ), sim_filter=TunableSimFilter.TunableReference( description= '\n Filter for specifying who to set at target Sims for loot\n application.\n ' ), max_sims=OptionalTunable( description= '\n If enabled and the Sim filter finds more than the specified\n number of Sims, the loot will only be applied a random\n selection of this many Sims.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), loot=LootActions.TunableReference( description= '\n Loot actions to apply to the two Sims. The Sim in the \n career is Actor, and if Targeted is enabled those Sims\n will be TargetSim.\n ' ))) } runtime_commodity = None @classmethod def _tuning_loaded_callback(cls): if cls.runtime_commodity is not None: return commodity = RuntimeCommodity.generate(cls.__name__) commodity.decay_rate = 0 commodity.convergence_value = 0 commodity.remove_on_convergence = True commodity.visible = False commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK commodity.min_value_tuning = 0 commodity.initial_value = 0 commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP cls.runtime_commodity = commodity def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._update_alarm_handle = None self._last_update_time = None def run(self, callback): super().run(callback) self._last_update_time = services.time_service().sim_now time_span = clock.interval_in_sim_minutes( Career.CAREER_PERFORMANCE_UPDATE_INTERVAL) self._update_alarm_handle = alarms.add_alarm( self, time_span, lambda alarm_handle: self._update(), repeating=True) def stop(self): if self._update_alarm_handle is not None: alarms.cancel_alarm(self._update_alarm_handle) self._update_alarm_handle = None self._update() super().stop() def _update(self): career = self.sim_info.career_tracker.get_at_work_career() if career is None: logger.error( 'CareerTone {} trying to update performance when Sim {} not at work', self, self.sim_info, owner='tingyul') return if career._upcoming_gig is not None and career._upcoming_gig.odd_job_tuning is not None: return now = services.time_service().sim_now elapsed = now - self._last_update_time self._last_update_time = now career.apply_performance_change(elapsed, self.performance_multiplier) career.resend_career_data() resolver = SingleSimResolver(self.sim_info) for entry in self.periodic_sim_filter_loot: chance = entry.chance.get_chance(resolver) * elapsed.in_hours() if random.random() > chance: continue services.sim_filter_service().submit_filter( entry.sim_filter, self._sim_filter_loot_response, callback_event_data=entry, requesting_sim_info=self.sim_info, gsi_source_fn=self.get_sim_filter_gsi_name) def get_sim_filter_gsi_name(self): return str(self) def _sim_filter_loot_response(self, filter_results, callback_event_data): entry = callback_event_data if entry.max_sims is None: targets = tuple(result.sim_info for result in filter_results) else: sample_size = min(len(filter_results), entry.max_sims) targets = tuple( result.sim_info for result in random.sample(filter_results, sample_size)) for target in targets: resolver = DoubleSimResolver(self.sim_info, target) entry.loot.apply_to_resolver(resolver) def apply_dominant_tone_loot(self): resolver = self.get_resolver() for loot in self.dominant_tone_loot_actions: loot.apply_to_resolver(resolver)
class Gig(HasTunableReference, _GigDisplayMixin, PrepTaskTrackerMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.CAREER_GIG)): INSTANCE_TUNABLES = { 'career': TunableReference( description= '\n The career this gig is associated with.\n ', manager=services.get_instance_manager( sims4.resources.Types.CAREER)), 'gig_time': WeeklySchedule.TunableFactory( description= '\n A tunable schedule that will determine when you have to be at work.\n ', export_modes=ExportModes.All), 'gig_prep_time': TunableTimeSpan( description= '\n The amount of time between when a gig is selected and when it\n occurs.\n ', default_hours=5), 'gig_prep_tasks': TunableList( description= '\n A list of prep tasks the Sim can do to improve their performance\n during the gig. \n ', tunable=PrepTask.TunableFactory()), 'loots_on_schedule': TunableList( description= '\n Loot actions to apply when a sim gets a gig.\n ', tunable=LootActions.TunableReference()), 'audio_on_prep_task_completion': OptionalTunable( description= '\n A sting to play at the time a prep task completes.\n ', tunable=TunablePlayAudio( locked_args={ 'immediate_audio': True, 'joint_name_hash': None, 'play_on_active_sim_only': True })), 'gig_pay': TunableVariant( description= '\n Base amount of pay for this gig. Can be either a flat amount or a\n range.\n ', range=TunableInterval(tunable_type=int, default_lower=0, default_upper=100, minimum=0), flat_amount=TunableIntervalLiteral(tunable_type=int, default=0, minimum=0), default='range'), 'additional_pay_per_overmax_level': OptionalTunable( description= '\n If checked, overmax levels will be considered when calculating pay\n for this gig. The actual implementation of this may vary by gig\n type.\n ', tunable=TunableRange(tunable_type=int, default=0, minimum=0)), 'result_based_gig_pay_multipliers': OptionalTunable( description= '\n A set of multipliers for gig pay. The multiplier used depends on the\n GigResult of the gig. The meanings of each GigResult may vary by\n gig type.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the additional pay\n the sim will receive.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory())), 'initial_result_based_career_performance': OptionalTunable( description= "\n A mapping between the GigResult for this gig and the initial\n career performance for the Sim's first gig.\n ", tunable=TunableMapping( description= '\n A map between the result type of the gig and the initial career\n performance the Sim will receive.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=Tunable( description= '\n The initial performance value that will be applied.\n ', tunable_type=float, default=0))), 'result_based_career_performance': OptionalTunable( description= '\n A mapping between the GigResult for this gig and the change in\n career performance for the sim.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the career\n performance the sim will receive.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=Tunable( description= '\n The performance modification.\n ', tunable_type=float, default=0))), 'result_based_career_performance_multiplier': OptionalTunable( description= '\n A mapping between the GigResult and the multiplier for the career \n performance awarded.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the career\n performance multiplier.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The performance modification multiplier.\n ' ))), 'result_based_loots': OptionalTunable( description= '\n A mapping between the GigResult for this gig and a loot list to\n optionally apply. The resolver for this loot list is either a\n SingleSimResolver of the working sim or a DoubleSimResolver with the\n target being the customer if there is a customer sim.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the loot list.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableList( description= '\n Loot actions to apply.\n ', tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)))), 'payout_stat_data': TunableMapping( description= '\n Stats, and its associated information, that are gained (or lost) \n when sim finishes this gig.\n ', key_type=TunableReference( description= '\n Stat for this payout.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), value_type=TunableTuple( description= '\n Data about this payout stat. \n ', base_amount=Tunable( description= '\n Base amount (pre-modifiers) applied to the sim at the end\n of the gig.\n ', tunable_type=float, default=0.0), medal_to_payout=TunableMapping( description= '\n Mapping of medal -> stat multiplier.\n ', key_type=TunableEnumEntry( description= '\n Medal achieved in this gig.\n ', tunable_type=SituationMedal, default=SituationMedal.TIN), value_type=TunableMultiplier.TunableFactory( description= '\n Mulitiplier on statistic payout if scorable situation\n ends with the associate medal.\n ' )), ui_threshold=TunableList( description= '\n Thresholds and icons we use for this stat to display in \n the end of day dialog. Tune in reverse of highest threshold \n to lowest threshold.\n ', tunable=TunableTuple( description= '\n Threshold and icon for this stat and this gig.\n ', threshold_icon=TunableIcon( description= '\n Icon if the stat is of this threshold.\n ' ), threshold_description=TunableLocalizedStringFactory( description= '\n Description to use with icon\n ' ), threshold=Tunable( description= '\n Threshold that the stat must >= to.\n ', tunable_type=float, default=0.0))))), 'career_events': TunableList( description= '\n A list of available career events for this gig.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.CAREER_EVENT))), 'gig_cast_rel_bit_collection_id': TunableEnumEntry( description= '\n If a rel bit is applied to the cast member, it must be of this collection id.\n We use this to clear the rel bit when the gig is over.\n ', tunable_type=RelationshipBitCollectionUid, default=RelationshipBitCollectionUid.Invalid, invalid_enums=(RelationshipBitCollectionUid.All, )), 'gig_cast': TunableList( description= '\n This is the list of sims that need to spawn for this gig. \n ', tunable=TunableTuple( description= '\n Data for cast members. It contains a test which tests against \n the owner of this gig and spawn the necessary sims. A bit\n may be applied through the loot action to determine the type \n of cast members (costars, directors, etc...) \n ', filter_test=TunableTestSet( description= '\n Test used on owner sim.\n ' ), sim_filter=TunableSimFilter.TunableReference( description= '\n If filter test is passed, this sim is created and stored.\n ' ), cast_member_rel_bit=OptionalTunable( description= '\n If tuned, this rel bit will be applied on the spawned cast \n member.\n ', tunable=RelationshipBit.TunableReference( description= '\n Rel bit to apply.\n ' )))), 'end_of_gig_dialog': OptionalTunable( description= '\n A results dialog to show. This dialog allows a list\n of icons with labels. Stats are added at the end of this icons.\n ', tunable=UiDialogLabeledIcons.TunableFactory()), 'disabled_tooltip': OptionalTunable( description= '\n If tuned, the tooltip when this row is disabled.\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'end_of_gig_notifications': OptionalTunable( description= '\n If enabled, a notification to show at the end of the gig instead of\n a normal career message. Tokens are:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The pay for the gig\n * 5: The gratuity for the gig\n * 6: The customer (sim) of the gig, if there is a customer.\n * 7: A bullet list of loots and payments as a result of this gig.\n This list uses the text tuned on the loots themselves to create\n bullets for each loot. Those texts will generally have tokens 0\n and 1 be the subject and target sims (of the loot) but may\n have additional tokens depending on the type of loot.\n ', tunable=TunableMapping( description= '\n A map between the result type of the gig and the post-gig\n notification.\n ', key_type=TunableEnumEntry(tunable_type=GigResult, default=GigResult.SUCCESS), value_type=_get_career_notification_tunable_factory()), tuning_group=GroupNames.UI), 'end_of_gig_overmax_notification': OptionalTunable( description= '\n If tuned, the notification that will be used if the sim gains an\n overmax level during this gig. Will override the overmax\n notification in career messages. The following tokens are provided:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The overmax level\n * 5: The pay for the gig\n * 6: Additional pay tuned at additional_pay_per_overmax_level \n * 7: The overmax rewards in a bullet-point list, in the form of a\n string. These are tuned on the career_track\n ', tunable=_get_career_notification_tunable_factory(), tuning_group=GroupNames.UI), 'end_of_gig_overmax_rewardless_notification': OptionalTunable( description= '\n If tuned, the notification that will be used if the sim gains an\n overmax level with no reward during this gig. Will override the\n overmax rewardless notification in career messages.The following\n tokens are provided:\n * 0: The Sim owner of the career\n * 1: The level name (e.g. Chef)\n * 2: The career name (e.g. Culinary)\n * 3: The company name (e.g. Maids United)\n * 4: The overmax level\n * 5: The pay for the gig\n * 6: Additional pay tuned at additional_pay_per_overmax_level \n ', tunable=_get_career_notification_tunable_factory(), tuning_group=GroupNames.UI), 'end_of_gig_promotion_text': OptionalTunable( description= '\n A string that, if enabled, will be pre-pended to the bullet\n list of results in the promotion notification. Tokens are:\n * 0 : The Sim owner of the career\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'end_of_gig_demotion_text': OptionalTunable( description= '\n A string that, if enabled, will be pre-pended to the bullet\n list of results in the promotion notification. Tokens are:\n * 0 : The Sim owner of the career\n ', tunable=TunableLocalizedStringFactory(), tuning_group=GroupNames.UI), 'odd_job_tuning': OptionalTunable( description= '\n Tuning specific to odd jobs. Leave untuned if this gig is not an\n odd job.\n ', tunable=TunableTuple( customer_description=TunableLocalizedStringFactory( description= '\n The description of the odd job written by the customer.\n Token 0 is the customer sim.\n ' ), use_customer_description_as_gig_description=Tunable( description= '\n If checked, the customer description will be used as the\n gig description. This description is used as the tooltip\n for the gig icon in the career panel.\n ', tunable_type=bool, default=False), result_based_gig_gratuity_multipliers=TunableMapping( description= '\n A set of multipliers for the gig gratuity. This maps the\n result type of the gig and the gratuity multiplier (a \n percentage). The base pay will be multiplied by this \n multiplier in order to determine the actual gratuity \n amount.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the \n Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The gratuity multiplier to be calculated for this \n GigResult.\n ' )), result_based_gig_gratuity_chance_multipliers=TunableMapping( description= '\n A set of multipliers for determining the gig gratuity \n chance (i.e., the probability the Sim will receive gratuity \n in addition to the base pay). The gratuity chance depends \n on the GigResult of the gig. This maps the result type of \n the gig and the gratuity chance/percentage. If this map\n (or a GigResult) is left untuned, then no gratuity is \n added.\n ', key_type=TunableEnumEntry( description= '\n The GigResult enum that represents the outcome of the \n Gig.\n ', tunable_type=GigResult, default=GigResult.SUCCESS), value_type=TunableMultiplier.TunableFactory( description= '\n The multiplier to be calculated for this GigResult. \n This represents the percentage chance the Sim will \n receive gratuity. If the Sim is to not receive \n gratuity, the base value should be 0 (without further\n tests). If this Sim is guaranteed to receive gratuity,\n the base value should be 1 (without further tests).\n ' )), gig_gratuity_bullet_point_text=OptionalTunable( description= '\n If enabled, the gig gratuity will be a bullet point in the\n bullet pointed list of loots and money supplied to the end\n of gig notification (this is token 7 of that notification).\n If disabled, gratuity will be omitted from that list.\n Tokens:\n * 0: The sim owner of the career\n * 1: The customer\n * 2: the gratuity amount \n ', tunable=TunableLocalizedStringFactory()))), 'tip': OptionalTunable( description= '\n A tip that is displayed with the gig in pickers and in the career\n panel. Can produce something like "Required Skill: Fitness 2".\n ', tunable=TunableTuple( tip_title=TunableLocalizedStringFactory( description= '\n The title string of the tip. Could be something like "Required\n Skill.\n ' ), tip_text=TunableLocalizedStringFactory( description= '\n The text string of the tip. Could be something like "Fitness 2".\n ' ), tip_icon=OptionalTunable(tunable=TunableIcon( description= '\n An icon to show along with the tip.\n ' )))), 'critical_failure_test': OptionalTunable( description= '\n The tests for checking whether or not the Sim should receive the \n CRITICAL_FAILURE outcome. This will override other GigResult \n behavior.\n ', tunable=TunableTestSet( description= '\n The tests to be performed on the Sim (and any customer). If \n the tests pass, the outcome will be CRITICAL_FAILURE. \n ' )) } def __init__(self, owner, customer=None, *args, **kwargs): super().__init__(*args, **kwargs) self._owner = owner self._customer_id = customer.id if customer is not None else None self._upcoming_gig_time = None self._gig_result = None self._gig_pay = None self._gig_gratuity = None self._loot_strings = None self._gig_attended = False @classmethod def get_aspiration(cls): pass @classmethod def get_time_until_next_possible_gig(cls, starting_time): required_prep_time = cls.gig_prep_time() start_considering_prep = starting_time + required_prep_time (time_until, _) = cls.gig_time().time_until_next_scheduled_event( start_considering_prep) if not time_until: return return time_until + required_prep_time def register_aspiration_callbacks(self): aspiration = self.get_aspiration() if aspiration is None: return aspiration.register_callbacks() aspiration_tracker = self._owner.aspiration_tracker if aspiration_tracker is None: return aspiration_tracker.validate_and_return_completed_status(aspiration) aspiration_tracker.process_test_events_for_aspiration(aspiration) def notify_gig_attended(self): self._gig_attended = True def has_attended_gig(self): return self._gig_attended def notify_canceled(self): self._gig_result = GigResult.CANCELED self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_CANCEL) def get_career_performance(self, first_gig=False): if not self.result_based_career_performance: return 0 if self.initial_result_based_career_performance is not None and first_gig and self._gig_result in self.initial_result_based_career_performance: return self.initial_result_based_career_performance[ self._gig_result] performance_modifier = 1 if self.result_based_career_performance_multiplier: if self._gig_result in self.result_based_career_performance_multiplier: resolver = self.get_resolver_for_gig() performance_modifier = self.result_based_career_performance_multiplier[ self._gig_result].get_multiplier(resolver) return self.result_based_career_performance.get( self._gig_result, 0) * performance_modifier def treat_work_time_as_due_date(self): return False @classmethod def create_picker_row(cls, description=None, scheduled_time=None, owner=None, gig_customer=None, enabled=True, **kwargs): tip = cls.tip description = cls.gig_picker_localization_format( cls.gig_pay.lower_bound, cls.gig_pay.upper_bound, scheduled_time, tip.tip_title(), tip.tip_text(), gig_customer) if not enabled and cls.disabled_tooltip is not None: row_tooltip = lambda *_: cls.disabled_tooltip(owner) elif cls.display_description is None: row_tooltip = None else: row_tooltip = lambda *_: cls.display_description(owner) if cls.odd_job_tuning is not None: customer_description = cls.odd_job_tuning.customer_description( gig_customer) row = OddJobPickerRow(customer_id=gig_customer.id, customer_description=customer_description, tip_title=tip.tip_title(), tip_text=tip.tip_text(), tip_icon=tip.tip_icon, name=cls.display_name(owner), icon=cls.display_icon, row_description=description, row_tooltip=row_tooltip, is_enable=enabled) else: row = ObjectPickerRow(name=cls.display_name(owner), icon=cls.display_icon, row_description=description, row_tooltip=row_tooltip, is_enable=enabled) return row def get_gig_time(self): return self._upcoming_gig_time def get_gig_customer(self): return self._customer_id def clean_up_gig(self): if self.gig_prep_tasks: self.prep_task_cleanup() def save_gig(self, gig_proto_buff): gig_proto_buff.gig_type = self.guid64 gig_proto_buff.gig_time = self._upcoming_gig_time if hasattr(gig_proto_buff, 'gig_attended'): gig_proto_buff.gig_attended = self._gig_attended if self._customer_id: gig_proto_buff.customer_sim_id = self._customer_id def load_gig(self, gig_proto_buff): self._upcoming_gig_time = DateAndTime(gig_proto_buff.gig_time) if self.gig_prep_tasks: self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64, self.audio_on_prep_task_completion, from_load=True) if gig_proto_buff.HasField('customer_sim_id'): self._customer_id = gig_proto_buff.customer_sim_id if gig_proto_buff.HasField('gig_attended'): self._gig_attended = gig_proto_buff.gig_attended def set_gig_time(self, upcoming_gig_time): self._upcoming_gig_time = upcoming_gig_time def get_resolver_for_gig(self): if self._customer_id is not None: customer_sim_info = services.sim_info_manager().get( self._customer_id) if customer_sim_info is not None: return DoubleSimResolver(self._owner, customer_sim_info) return SingleSimResolver(self._owner) def set_up_gig(self): if self.gig_prep_tasks: self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64, self.audio_on_prep_task_completion) if self.loots_on_schedule: resolver = self.get_resolver_for_gig() for loot_actions in self.loots_on_schedule: loot_actions.apply_to_resolver(resolver) self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_STARTED) def collect_rabbit_hole_rewards(self): pass def _get_additional_loots(self): if self.result_based_loots is not None and self._gig_result is not None: loots = self.result_based_loots.get(self._gig_result) if loots is not None: return loots return () def collect_additional_rewards(self): loots = self._get_additional_loots() if loots: self._loot_strings = [] resolver = self.get_resolver_for_gig() for loot_actions in loots: self._loot_strings.extend( loot_actions.apply_to_resolver_and_get_display_texts( resolver)) def _determine_gig_outcome(self): raise NotImplementedError def get_pay(self, overmax_level=None, **kwargs): self._determine_gig_outcome() pay = self.gig_pay.lower_bound if self.additional_pay_per_overmax_level: pay = pay + overmax_level * self.additional_pay_per_overmax_level resolver = self.get_resolver_for_gig() if self.result_based_gig_pay_multipliers: if self._gig_result in self.result_based_gig_pay_multipliers: multiplier = self.result_based_gig_pay_multipliers[ self._gig_result].get_multiplier(resolver) pay = int(pay * multiplier) gratuity = 0 if self.odd_job_tuning: if self.odd_job_tuning.result_based_gig_gratuity_multipliers: if self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers: gratuity_multiplier = 0 gratuity_chance = 0 if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers: gratuity_chance = self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers[ self._gig_result].get_multiplier(resolver) if random.random() <= gratuity_chance: if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_multipliers: gratuity_multiplier = self.odd_job_tuning.result_based_gig_gratuity_multipliers[ self._gig_result].get_multiplier(resolver) gratuity = int(pay * gratuity_multiplier) self._gig_pay = pay self._gig_gratuity = gratuity return pay + gratuity def get_promotion_evaluation_result(self, reward_text, *args, first_gig=False, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) if self.end_of_gig_promotion_text and not first_gig: results_list = self.get_results_list( self.end_of_gig_promotion_text(self._owner)) else: results_list = self.get_results_list() return EvaluationResult(Evaluation.PROMOTED, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, results_list) def get_demotion_evaluation_result(self, *args, first_gig=False, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) if self.end_of_gig_demotion_text and not first_gig: results_list = self.get_results_list( self.end_of_gig_demotion_text(self._owner)) else: results_list = self.get_results_list() return EvaluationResult(Evaluation.DEMOTED, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, results_list) def get_overmax_evaluation_result(self, overmax_level, reward_text, *args, **kwargs): if reward_text and self.end_of_gig_overmax_notification: return EvaluationResult(Evaluation.PROMOTED, self.end_of_gig_overmax_notification, overmax_level, self._gig_pay, self.additional_pay_per_overmax_level, reward_text) elif self.end_of_gig_overmax_rewardless_notification: return EvaluationResult( Evaluation.PROMOTED, self.end_of_gig_overmax_rewardless_notification, overmax_level, self._gig_pay, self.additional_pay_per_overmax_level) def _get_strings_for_results_list(self): strings = [] if self._gig_pay is not None: strings.append(LocalizationHelperTuning.MONEY(self._gig_pay)) if self.odd_job_tuning is not None and self._gig_gratuity: gratuity_text_factory = self.odd_job_tuning.gig_gratuity_bullet_point_text if gratuity_text_factory is not None: customer_sim_info = services.sim_info_manager().get( self._customer_id) gratuity_text = gratuity_text_factory(self._owner, customer_sim_info, self._gig_gratuity) strings.append(gratuity_text) if self._loot_strings: strings.extend(self._loot_strings) return strings def get_results_list(self, *additional_tokens): return LocalizationHelperTuning.get_bulleted_list( None, *additional_tokens, *self._get_strings_for_results_list()) def get_end_of_gig_evaluation_result(self, **kwargs): if self._gig_result is not None and self.end_of_gig_notifications is not None: notification = self.end_of_gig_notifications.get( self._gig_result, None) if notification: customer_sim_info = services.sim_info_manager().get( self._customer_id) return EvaluationResult(Evaluation.ON_TARGET, notification, self._gig_pay, self._gig_gratuity, customer_sim_info, self.get_results_list()) @classmethod def _get_base_pay_for_gig_owner(cls, owner): overmax_pay = cls.additional_pay_per_overmax_level if overmax_pay is not None: career = owner.career_tracker.get_career_by_uid(cls.career.guid64) if career is not None: overmax_pay *= career.overmax_level return (cls.gig_pay.lower_bound + overmax_pay, cls.gig_pay.upper_bound + overmax_pay) return (cls.gig_pay.lower_bound, cls.gig_pay.upper_bound) @classmethod def build_gig_msg(cls, msg, sim, gig_time=None, gig_customer=None): msg.gig_type = cls.guid64 msg.gig_name = cls.display_name(sim) (pay_lower, pay_upper) = cls._get_base_pay_for_gig_owner(sim) msg.min_pay = pay_lower msg.max_pay = pay_upper msg.gig_icon = ResourceKey() msg.gig_icon.instance = cls.display_icon.instance msg.gig_icon.group = cls.display_icon.group msg.gig_icon.type = cls.display_icon.type if cls.odd_job_tuning is not None and cls.odd_job_tuning.use_customer_description_as_gig_description and gig_customer is not None: customer_sim_info = services.sim_info_manager().get(gig_customer) if customer_sim_info is not None: msg.gig_description = cls.odd_job_tuning.customer_description( customer_sim_info) else: msg.gig_description = cls.display_description(sim) if gig_time is not None: msg.gig_time = gig_time if gig_customer is not None: msg.customer_id = gig_customer if cls.tip is not None: msg.tip_title = cls.tip.tip_title() if cls.tip.tip_icon is not None or cls.tip.tip_text is not None: build_icon_info_msg( IconInfoData(icon_resource=cls.tip.tip_icon), None, msg.tip_icon, desc=cls.tip.tip_text()) def send_prep_task_update(self): if self.gig_prep_tasks: self._prep_task_tracker.send_prep_task_update() def _apply_payout_stat(self, medal, payout_display_data=None): owner_sim = self._owner resolver = SingleSimResolver(owner_sim) payout_stats = self.payout_stat_data for stat in payout_stats.keys(): stat_tracker = owner_sim.get_tracker(stat) if not owner_sim.get_tracker(stat).has_statistic(stat): continue stat_data = payout_stats[stat] stat_multiplier = 1.0 if medal in stat_data.medal_to_payout: multiplier = stat_data.medal_to_payout[medal] stat_multiplier = multiplier.get_multiplier(resolver) stat_total = stat_data.base_amount * stat_multiplier stat_tracker.add_value(stat, stat_total) if payout_display_data is not None: for threshold_data in stat_data.ui_threshold: if stat_total >= threshold_data.threshold: payout_display_data.append(threshold_data) break def _send_gig_telemetry(self, progress): with telemetry_helper.begin_hook(gig_telemetry_writer, TELEMETRY_HOOK_GIG_PROGRESS, sim_info=self._owner) as hook: hook.write_int(TELEMETRY_CAREER_ID, self.career.guid64) hook.write_int(TELEMETRY_GIG_ID, self.guid64) hook.write_int(TELEMETRY_GIG_PROGRESS_NUMBER, progress)
class CareerEvent(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.CAREER_EVENT)): INSTANCE_TUNABLES = {'required_zone': RequiredCareerEventZoneTunableVariant(description='\n The required zone for this career event (e.g. the hospital lot for\n a doctor career event). The Sim involved in this event will\n automatically travel to this zone at the beginning of the work\n shift. The Sim will in general be prohibited from leaving this zone\n without work -- the lone exception is the Sim is allowed to travel\n for a career sub-event (e.g. a detective Sim running a career event\n requiring the police station lot is allowed to initiate the sub-\n event of investigating the crime scene at a commercial lot).\n '), 'zone_director': OptionalTunable(description='\n An optional zone director to apply to the zone the career event\n takes place.\n ', tunable=CareerEventZoneDirector.TunableReference()), 'scorable_situation': OptionalTunable(description='\n A situation which the player must complete. Work performance for\n the Sim will depend on how much the Sim accomplishes. This should\n be enabled for main events and disabled for sub events. Example:\n \n Detective Career. The career event that starts at the beginning of\n the work shift, going to the police station, will have a scorable\n situation. The sub event to go to the crime scene will not, as the\n career event will not be scored against it.\n ', tunable=TunableTuple(situation=TunableReference(description='\n Situation which the Sim in the career event will be scored by.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), allow_none=True, class_restrictions=('CareerEventSituation',)), medal_payout_tin=MedalPayout(description='\n Work performance and money payout if scorable situation\n ends with a tin medal.\n '), medal_payout_bronze=MedalPayout(description='\n Work performance and money payout if scorable situation\n ends with a bronze medal.\n '), medal_payout_silver=MedalPayout(description='\n Work performance and money payout if scorable situation\n ends with a silver medal.\n '), medal_payout_gold=MedalPayout(description='\n Work performance and money payout if scorable situation\n ends with a gold medal.\n ')), enabled_by_default=True, disabled_value=None, disabled_name='sub_event', enabled_name='main_event'), 'tests': TunableTestSet(description='\n Tests for if this career event is available to the Sim\n ParticipantType.Actor.\n '), 'loot_on_request': LootActions.TunableReference(description='\n Loot applied when the career event is requested to start. Happens\n before traveling.\n \n Example 1: A detective is at home and goes to work. This loot\n applies while the detective is still on the home lot, right before\n the travel to the police station happens.\n \n Example 2: A detective at the police station travels to a crime\n scene. This loot for the crime scene sub event applies while the\n detective is still at the police station, right before the travel.\n ', allow_none=True), 'loot_on_start': LootActions.TunableReference(description='\n Loot applied when the career event starts. Happens after travel.\n \n Example 1: A detective is at home and goes to work. This loot \n applies at the police station, right after traveling.\n \n Example 2: A detective at the police station travels to a crime\n scene. This loot for the crime scene sub event applies at the\n crime scene lot, right after traveling.\n ', allow_none=True), 'loots_on_end': TunableList(description='\n Loots applied when the career event ends.\n ', tunable=LootActions.TunableReference(description='\n A loot applied when the career event ends.\n ', allow_none=True, pack_safe=True)), 'cooldown': TunableRange(description='\n How many work days before this career event will be offered again.\n ', tunable_type=int, minimum=0, default=0)} def __init__(self, career): self._career = career self._required_zone_id = None self._event_situation_id = 0 self._state = CareerEventState.CREATED @property def sim_info(self): return self._career.sim_info @property def career(self): return self._career def on_career_event_requested(self): self._advance_state(CareerEventState.REQUESTED) self._required_zone_id = self.required_zone.get_required_zone_id(self.sim_info) if self.loot_on_request is not None: resolver = SingleSimResolver(self._career.sim_info) self.loot_on_request.apply_to_resolver(resolver) def on_career_event_start(self): self._advance_state(CareerEventState.RUNNING) if self.loot_on_start is not None: resolver = SingleSimResolver(self._career.sim_info) self.loot_on_start.apply_to_resolver(resolver) def on_career_event_stop(self): self._advance_state(CareerEventState.STOPPED) resolver = SingleSimResolver(self._career.sim_info) for loot in self.loots_on_end: if loot is not None: loot.apply_to_resolver(resolver) curr_zone = services.venue_service().get_zone_director() if curr_zone and self.zone_director and self.zone_director.guid64 == curr_zone.guid64: curr_zone.on_career_event_stop() def request_zone_director(self): if self.zone_director is not None: zone_director = self.zone_director(career_event=self) venue_service = services.venue_service() if venue_service.has_zone_director: venue_service.change_zone_director(zone_director, True) else: preserve_state = self._state >= CareerEventState.RUNNING venue_service.request_zone_director(zone_director, ZoneDirectorRequestType.CAREER_EVENT, preserve_state=preserve_state) def get_event_situation_id(self): return self._event_situation_id def set_event_situation_id(self, event_situation_id): self._event_situation_id = event_situation_id def get_required_zone_id(self): return self._required_zone_id def _advance_state(self, state): logger.assert_log(state > self._state, 'Going backwards when trying to advance state. Old: {}, New: {}', self._state, state) self._state = state def start_from_zone_spin_up(self): if self._state == CareerEventState.REQUESTED: self.on_career_event_start() def get_career_event_data_proto(self): proto = SimObjectAttributes_pb2.CareerEventData() proto.career_event_id = self.guid64 proto.event_situation_id = self._event_situation_id if self._required_zone_id is not None: proto.required_zone_id = self._required_zone_id proto.state = self._state return proto def load_from_career_event_data_proto(self, proto): self._event_situation_id = proto.event_situation_id if has_field(proto, 'required_zone_id'): self._required_zone_id = proto.required_zone_id self._state = CareerEventState(proto.state)
class AdventureMoment(HasTunableFactory, AutoFactoryInit): LOOT_NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n A string used to recursively build loot notification text. It will be\n given two tokens: a loot display text string, if any, and the previously\n built LOOT_NOTIFICATION_TEXT string.\n ') NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n A string used to format notifications. It will be given two arguments:\n the notification text and the built version of LOOT_NOTIFICATION_TEXT,\n if not empty.\n ') CHEAT_TEXT = TunableTuple(description='\n Strings to be used for display text on cheat buttons to trigger all\n adventure moments. \n ', previous_display_text=TunableLocalizedStringFactory(description='\n Text that will be displayed on previous cheat button.\n '), next_display_text=TunableLocalizedStringFactory(description='\n Text that will be displayed on next cheat button.\n '), text_pattern=TunableLocalizedStringFactory(description='\n Format for displaying next and previous buttons text including the\n progress.\n '), tooltip=TunableLocalizedStringFactory(description='\n Tooltip to show when disabling previous or next button.\n ')) COST_TYPE_SIMOLEONS = 0 COST_TYPE_ITEMS = 1 CHEAT_PREVIOUS_INDEX = 1 CHEAT_NEXT_INDEX = 2 FACTORY_TUNABLES = {'description': '\n A phase of an adventure. Adventure moments may present\n some information in a dialog form and for a choice to be\n made regarding how the overall adventure will branch.\n ', '_visibility': OptionalTunable(description='\n Control whether or not this moment provides visual feedback to\n the player (i.e., a modal dialog).\n ', tunable=UiDialog.TunableFactory(), disabled_name='not_visible', enabled_name='show_dialog'), '_finish_actions': TunableList(description='\n A list of choices that can be made by the player to determine\n branching for the adventure. They will be displayed as buttons\n in the UI. If no dialog is displayed, then the first available\n finish action will be selected. If this list is empty, the\n adventure ends.\n ', tunable=TunableTuple(availability_tests=TunableTestSet(description='\n A set of tests that must pass in order for this Finish\n Action to be available on the dialog. A Finish Action failing\n all tests is handled as if it were never tuned.\n '), display_text=TunableLocalizedStringFactoryVariant(description="\n This finish action's title. This will be the button text in\n the UI.\n ", allow_none=True), display_subtext=TunableLocalizedStringFactoryVariant(description='\n If tuned, this text will display below the button for this Finish Action.\n \n Span tags can be used to change the color of the text to green/positive and red/negative.\n <span class="positive">TEXT</span> will make the word TEXT green\n <span class="negative">TEXT</span> will make the word TEXT red\n ', allow_none=True), disabled_text=OptionalTunable(description="\n If enabled, this is the string that will be displayed if \n this finishing action is not available because the tests \n don't pass.\n ", tunable=TunableLocalizedStringFactory()), cost=TunableVariant(description='\n The cost associated with this finish action. Only one type\n of cost may be tuned. The player is informed of the cost\n before making the selection by modifying the display_text\n string to include this information.\n ', simoleon_cost=TunableTuple(description="The specified\n amount will be deducted from the Sim's funds.\n ", locked_args={'cost_type': COST_TYPE_SIMOLEONS}, amount=TunableRange(description='How many Simoleons to\n deduct.\n ', tunable_type=int, default=0, minimum=0)), item_cost=TunableTuple(description="The specified items will \n be removed from the Sim's inventory.\n ", locked_args={'cost_type': COST_TYPE_ITEMS}, item_cost=ItemCost.TunableFactory()), default=None), action_results=TunableList(description='\n A list of possible results that can occur if this finish\n action is selected. Action results can award loot, display\n notifications, and control the branching of the adventure by\n selecting the next adventure moment to run.\n ', tunable=TunableTuple(weight_modifiers=TunableList(description='\n A list of modifiers that affect the probability that\n this action result will be chosen. These are exposed\n in the form (test, multiplier). If the test passes,\n then the multiplier is applied to the running total.\n The default multiplier is 1. To increase the\n likelihood of this action result being chosen, tune\n multiplier greater than 1. To decrease the\n likelihood of this action result being chose, tune\n multipliers lower than 1. If you want to exclude\n this action result from consideration, tune a\n multiplier of 0.\n ', tunable=TunableTuple(description='\n A pair of test and weight multiplier. If the\n test passes, the associated weight multiplier is\n applied. If no test is specified, the multiplier\n is always applied.\n ', test=TunableTestVariant(description='\n The test that has to pass for this weight\n multiplier to be applied. The information\n available to this test is the same\n information available to the interaction\n owning this adventure.\n ', test_locked_args={'tooltip': None}), weight_multiplier=Tunable(description='\n The weight multiplier to apply if the\n associated test passes.\n ', tunable_type=float, default=1))), notification=OptionalTunable(description='\n If set, this notification will be displayed.\n ', tunable=TunableUiDialogNotificationSnippet()), next_moments=TunableList(description='\n A list of adventure moment keys. One of these keys will\n be selected to determine which adventure moment is\n selected next. If the list is empty, the adventure ends\n here. Any of the keys tuned here will have to be tuned\n in the _adventure_moments tunable for the owning adventure.\n ', tunable=AdventureMomentKey), loot_actions=TunableList(description='\n List of Loot actions that are awarded if this action result is selected.\n ', tunable=LootActions.TunableReference()), continuation=TunableContinuation(description='\n A continuation to push when running finish actions.\n '), results_dialog=OptionalTunable(description='\n A results dialog to show. This dialog allows a list\n of icons with labels.\n ', tunable=UiDialogLabeledIcons.TunableFactory()), events_to_send=TunableList(description='\n A list of events to send.\n ', tunable=TunableEnumEntry(description='\n events types to send\n ', tunable_type=TestEvent, default=TestEvent.Invalid))))))} def __init__(self, parent_adventure, **kwargs): super().__init__(**kwargs) self._parent_adventure = parent_adventure self.resolver = self._interaction.get_resolver() @property def _interaction(self): return self._parent_adventure.interaction @property def _sim(self): return self._interaction.sim def run_adventure(self): if self._visibility is None: if self._finish_actions: self._run_first_valid_finish_action() else: dialog = self._get_dialog() if dialog is not None: self._parent_adventure.force_action_result = True dialog.show_dialog(auto_response=0) def _run_first_valid_finish_action(self): resolver = self.resolver for (action_id, finish_action) in enumerate(self._finish_actions): if finish_action.availability_tests.run_tests(resolver): return self._run_action_from_index(action_id) def _is_action_result_available(self, action_result): if not action_result.next_moments: return True for moment_key in action_result.next_moments: if self._parent_adventure.is_adventure_moment_available(moment_key): return True return False def _run_action_from_cheat(self, action_index): cheat_index = action_index - len(self._finish_actions) + 1 if cheat_index == self.CHEAT_PREVIOUS_INDEX: self._parent_adventure.run_cheat_previous_moment() elif cheat_index == self.CHEAT_NEXT_INDEX: self._parent_adventure.run_cheat_next_moment() def _get_action_result_weight(self, action_result): interaction_resolver = self.resolver weight = 1 for modifier in action_result.weight_modifiers: if not modifier.test is None: if interaction_resolver(modifier.test): weight *= modifier.weight_multiplier weight *= modifier.weight_multiplier return weight def _apply_action_cost(self, action): if action.cost.cost_type == self.COST_TYPE_SIMOLEONS: if not self._sim.family_funds.try_remove(action.cost.amount, Consts_pb2.TELEMETRY_INTERACTION_COST, sim=self._sim): return False elif action.cost.cost_type == self.COST_TYPE_ITEMS: item_cost = action.cost.item_cost return item_cost.consume_interaction_cost(self._interaction)() return True def _run_action_from_index(self, action_index): try: finish_action = self._finish_actions[action_index] except IndexError as err: logger.exception('Exception {} while attempting to get finish action.\nFinishActions length: {}, ActionIndex: {},\nCurrent Moment: {},\nResolver: {}.\n', err, len(self._finish_actions), action_index, self._parent_adventure._current_moment_key, self.resolver) return forced_action_result = False weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results if self._is_action_result_available(action_result)] if not weight_pairs: if self._parent_adventure.force_action_result: forced_action_result = True weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results] action_result = weighted_random_item(weight_pairs) if not (action_result is not None or not finish_action.action_results) and not self._apply_action_cost(finish_action): return if action_result is not None: loot_display_text = None resolver = self.resolver for actions in action_result.loot_actions: for (loot_op, test_ran) in actions.get_loot_ops_gen(resolver): (success, _) = loot_op.apply_to_resolver(resolver, skip_test=test_ran) if success and action_result.notification is not None: current_loot_display_text = loot_op.get_display_text() if current_loot_display_text is not None: if loot_display_text is None: loot_display_text = current_loot_display_text else: loot_display_text = self.LOOT_NOTIFICATION_TEXT(loot_display_text, current_loot_display_text) if action_result.notification is not None: if loot_display_text is not None: notification_text = lambda *tokens: self.NOTIFICATION_TEXT(action_result.notification.text(*tokens), loot_display_text) else: notification_text = action_result.notification.text dialog = action_result.notification(self._sim, self.resolver) dialog.text = notification_text dialog.show_dialog() if action_result.next_moments: if forced_action_result: next_moment_key = random.choice(action_result.next_moments) else: next_moment_key = random.choice(tuple(moment_key for moment_key in action_result.next_moments if self._parent_adventure.is_adventure_moment_available(moment_key))) self._parent_adventure.queue_adventure_moment(next_moment_key) if action_result.results_dialog: dialog = action_result.results_dialog(self._sim, resolver=self.resolver) dialog.show_dialog() event_manager = services.get_event_manager() for event_type in action_result.events_to_send: event_manager.process_event(event_type, sim_info=self._sim.sim_info) if action_result.continuation: self._interaction.push_tunable_continuation(action_result.continuation) def _is_cheat_response(self, response): cheat_response = response - len(self._finish_actions) if cheat_response < 0: return False return True def _on_dialog_response(self, dialog): response_index = dialog.response if response_index is None: return if False and self._is_cheat_response(response_index): self._run_action_from_cheat(response_index) return if response_index >= len(self._finish_actions): return self._run_action_from_index(response_index) def _get_action_display_text(self, action): display_name = self._interaction.create_localized_string(action.display_text) if action.cost is not None: if action.cost.cost_type == self.COST_TYPE_SIMOLEONS: amount = action.cost.amount display_name = self._interaction.SIMOLEON_COST_NAME_FACTORY(display_name, amount) elif action.cost.cost_type == self.COST_TYPE_ITEMS: item_cost = action.cost.item_cost display_name = item_cost.get_interaction_name(self._interaction, display_name) return lambda *_, **__: display_name def _get_dialog(self): resolver = self.resolver dialog = self._visibility(self._sim, resolver) responses = [] for (action_id, finish_action) in enumerate(self._finish_actions): result = finish_action.availability_tests.run_tests(resolver) if not result: if finish_action.disabled_text is not None: disabled_text = finish_action.disabled_text if not result else None responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None)) disabled_text = finish_action.disabled_text if not result else None responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None)) if not responses: return if False and _show_all_adventure_moments: responses.extend(self._parent_adventure.get_cheat_responses(action_id)) dialog.set_responses(responses) dialog.add_listener(self._on_dialog_response) return dialog
class WeatherAwareComponent(RouteEventProviderMixin, Component, HasTunableFactory, AutoFactoryInit, component_name=WEATHER_AWARE_COMPONENT): class TunableWeatherAwareMapping(TunableMapping): def __init__(self, start_description=None, end_description=None, **kwargs): super().__init__( key_type=TunableEnumEntry( description= '\n The weather type we are interested in.\n ', tunable_type=WeatherType, default=WeatherType.UNDEFINED), value_type=TunableTuple( start_loot=TunableList( description=start_description, tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)), end_loot=TunableList( description=end_description, tunable=LootActions. TunableReference( description= '\n The loot action applied.\n ', pack_safe=True))), **kwargs) self.cache_key = 'TunableWeatherAwareMapping' def load_etree_node(self, node, source, expect_error): value = super().load_etree_node(node, source, expect_error) modified_dict = {} for (weather_type, loots) in value.items(): if not loots.start_loot: if loots.end_loot: modified_dict[weather_type] = loots modified_dict[weather_type] = loots return frozendict(modified_dict) FACTORY_TUNABLES = { 'inside_loot': TunableWeatherAwareMapping( description= "\n A tunable mapping linking a weather type to the loot actions the \n component owner should get when inside.\n \n WeatherType will be UNDEFINED if weather isn't installed.\n ", start_description= '\n Loot actions the owner should get when the weather \n starts if inside or when the object moves inside \n during the specified weather.\n ', end_description= '\n Loot actions the owner should get when the weather \n ends if inside or when the object moves outside \n during the specified weather.\n ' ), 'outside_loot': TunableWeatherAwareMapping( description= "\n A tunable mapping linking a weather type to the loot actions the \n component owner should get when outside.\n \n WeatherType will be UNDEFINED if weather isn't installed.\n ", start_description= '\n Loot actions the owner should get when the weather \n starts if outside or when the object moves outside \n during the specified weather.\n ', end_description= '\n Loot actions the owner should get when the weather \n ends if outside or when the object moves inside \n during the specified weather.\n ' ), 'anywhere_loot': TunableWeatherAwareMapping( description= "\n A tunable mapping linking a weather type to the loot actions the \n component owner should get regardless of inside/outside location.\n \n WeatherType will be UNDEFINED if weather isn't installed.\n Anywhere actions happen after inside/outside actions when weather starts.\n Anywhere actions happen before inside/outside actions when weather ends.\n ", start_description= '\n Loot actions the owner should get when the weather \n starts regardless of location.\n ', end_description= '\n Loot actions the owner should get when the weather \n ends regardless of location.\n ' ), 'disable_loot': TunableList( description= '\n A list of loot actions to apply to the owner of this component when\n the component is disabled.\n ', tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)), 'enable_loot': TunableList( description= '\n A list of loot actions to apply to the owner of this component when\n the component is enabled.\n ', tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)), 'lightning_strike_loot': TunableList( description= '\n A list of loot actions to apply to the owner of this component when\n they are struck by lightning.\n ', tunable=LootActions.TunableReference( description= '\n The loot action applied.\n ', pack_safe=True)), 'umbrella_route_events': OptionalTunable( description= '\n If tuned, we will consider points around inside/outside threshold\n to handle umbrella route events.\n ', tunable=TunableTuple( description= '\n Data used to populate fields on the path plan context.\n ', enter_carry_event=RouteEvent.TunablePackSafeReference( description= '\n To be moved into weather aware component.\n Route event to trigger umbrella carry.\n ' ), exit_carry_event=RouteEvent.TunablePackSafeReference( description= '\n To be moved into weather aware component.\n Route event to trigger umbrella carry.\n ' ))), 'lightning_strike_multiplier': OptionalTunable( description= '\n If enabled, we will modify the chance that this object is struck by\n lightning. Note that the object must be tagged as an object that\n can be struck. See Lightning module tuning.\n ', tunable=TunableMultiplier.TunableFactory( description= '\n A multiplier to the weight for this object to be struck by\n lightning instead of other objects. \n \n Note that this affects Sims as well, but will affect the chance\n this Sim is struck vs. other Sims, not other objects.\n ' )), 'lightning_effect_override': OptionalTunable( description= '\n If enabled, when this object is struck by lightning we will play \n this tuned effect instead of the one specified in module lightning \n tuning.\n ', tunable=PlayEffect.TunableFactory( description= '\n The effect we want to play when this object is struck by \n lightning.\n ' )) } class LocationUpdateStatus(enum.Int, export=False): NOT_IN_PROGRESS = 0 IN_PROGRESS = 1 PENDING = 2 def __init__(self, *args, parent=True, **kwargs): super().__init__(*args, **kwargs) self._is_outside = None self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS self._inside_sensitive = True if self.outside_loot or self.inside_loot else False self._disabled_count = 0 self._safety_umbrella_putaway_event = None def is_valid_to_add(self): if not super().is_valid_to_add(): return False if not self.lightning_strike_loot: if self.umbrella_route_events is None: return False elif not self.umbrella_route_events.enter_carry_event and not self.umbrella_route_events.exit_carry_event: return False return True def enable(self, value): if not value: if self.enabled: if self.owner.manager is not None and services.current_zone( ).is_zone_running: self._update_location(disabling=True) if self.owner.is_sim: resolver = SingleSimResolver(self.owner.sim_info) else: resolver = SingleObjectResolver(self.owner) loot_ops_list = LootOperationList(resolver, self.disable_loot) loot_ops_list.apply_operations() self.on_remove() self._disabled_count += 1 else: self._disabled_count -= 1 if self.enabled: if self.owner.is_sim: resolver = SingleSimResolver(self.owner.sim_info) else: resolver = SingleObjectResolver(self.owner) loot_ops_list = LootOperationList(resolver, self.enable_loot) loot_ops_list.apply_operations() self.on_add() if self._inside_sensitive: self.on_location_changed_callback() elif self._disabled_count < 0: logger.error( 'Unbalanced enabled/disable in weathercomponent. Called disable once more than enable.' ) self._disabled_count = 0 @property def enabled(self): return self._disabled_count == 0 def on_add(self): if self.enabled: if self._inside_sensitive: self.owner.register_on_location_changed( self.on_location_changed_callback) else: self.on_location_changed_callback() if self._has_routing_events(): self.owner.routing_component.register_routing_stage_event( RoutingStageEvent.ROUTE_END, self._on_route_finished) def on_remove(self): if self.enabled: self._stop() if self.owner.is_on_location_changed_callback_registered( self.on_location_changed_callback): self.owner.unregister_on_location_changed( self.on_location_changed_callback) if self._has_routing_events(): self.owner.routing_component.unregister_routing_stage_event( RoutingStageEvent.ROUTE_END, self._on_route_finished) def _stop(self): weather_service = services.weather_service() if weather_service is not None: weather_service.flush_weather_aware_message(self.owner) if self._is_outside is not None: weather_types = set(self.anywhere_loot) if self._is_outside: weather_types.update(self.outside_loot) else: weather_types.update(self.inside_loot) weather_service.deregister_object(self.owner, weather_types) self._is_outside = None def on_added_to_inventory(self): if self.enabled: if not self._inside_sensitive: self.on_location_changed_callback() self._stop() def on_removed_from_inventory(self): if self.enabled: self.on_location_changed_callback() def on_finalize_load(self): if self.enabled and not self.owner.is_sim: self._update_location() def on_preroll_autonomy(self): if self.enabled: is_inside_override = False next_interaction = self.owner.queue.peek_head() if next_interaction is not None: if next_interaction.counts_as_inside: is_inside_override = True self._update_location(is_inside_override=is_inside_override) def on_buildbuy_exit(self): if self.enabled: self._update_location() def on_location_changed_callback(self, *_, **__): if self.enabled and self.owner.manager is not None and services.current_zone( ).is_zone_running: self._update_location() def _update_location(self, is_inside_override=False, disabling=False): if disabling: is_outside = None elif is_inside_override: is_outside = False elif self.owner.is_in_inventory(): is_outside = None elif not self._inside_sensitive: is_outside = True else: is_outside = self.owner.is_outside if is_outside == self._is_outside: return if self._location_update_status != WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS: self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.PENDING return self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.IN_PROGRESS was_outside = self._is_outside self._is_outside = is_outside recurse = False try: weather_service = services.weather_service() if weather_service is not None: weather_types = weather_service.get_current_weather_types() weather_service.update_weather_aware_message(self.owner) else: weather_types = {WeatherType.UNDEFINED} if self.owner.is_sim: resolver = SingleSimResolver(self.owner.sim_info) else: resolver = SingleObjectResolver(self.owner) if was_outside is not None: if was_outside: self._give_loot(weather_types, self.outside_loot, resolver, False) if weather_service is not None: weather_service.deregister_object( self.owner, self.outside_loot.keys()) else: self._give_loot(weather_types, self.inside_loot, resolver, False) if weather_service is not None: weather_service.deregister_object( self.owner, self.inside_loot.keys()) if is_outside is not None: if is_outside: self._give_loot(weather_types, self.outside_loot, resolver, True) if weather_service is not None: weather_service.register_object( self.owner, self.outside_loot.keys()) weather_service.register_object( self.owner, self.anywhere_loot.keys()) else: self._give_loot(weather_types, self.inside_loot, resolver, True) if weather_service is not None: weather_service.register_object( self.owner, self.inside_loot.keys()) weather_service.register_object( self.owner, self.anywhere_loot.keys()) if was_outside is None: self._give_loot(weather_types, self.anywhere_loot, resolver, True) else: self._give_loot(weather_types, self.anywhere_loot, resolver, False) if weather_service is not None: weather_service.deregister_object( self.owner, self.anywhere_loot.keys()) recurse = self._location_update_status == WeatherAwareComponent.LocationUpdateStatus.PENDING finally: self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS if recurse: self._update_location(is_inside_override=is_inside_override, disabling=disabling) def _give_loot(self, weather_types, loot_dict, resolver, is_start): for weather_type in weather_types & loot_dict.keys(): loot = loot_dict[ weather_type].start_loot if is_start else loot_dict[ weather_type].end_loot for loot_action in loot: loot_action.apply_to_resolver(resolver) @componentmethod_with_fallback(lambda *_, **__: None) def give_weather_loot(self, weather_types, is_start): if self._is_outside is not None: if self.owner.is_sim: resolver = SingleSimResolver(self.owner.sim_info) else: resolver = SingleObjectResolver(self.owner) if not is_start: self._give_loot(weather_types, self.anywhere_loot, resolver, is_start) if self._is_outside: self._give_loot(weather_types, self.outside_loot, resolver, is_start) else: self._give_loot(weather_types, self.inside_loot, resolver, is_start) if is_start: self._give_loot(weather_types, self.anywhere_loot, resolver, is_start) @componentmethod_with_fallback(lambda *_, **__: 1.0) def get_lightning_strike_multiplier(self): if not self.enabled: return 0 elif self.lightning_strike_multiplier is not None: return self.lightning_strike_multiplier.get_multiplier( SingleObjectResolver(self.owner)) return 1.0 def on_struck_by_lightning(self): loot_ops_list = LootOperationList(SingleObjectResolver(self.owner), self.lightning_strike_loot) loot_ops_list.apply_operations() def _on_route_finished(self, *_, **__): self._safety_umbrella_putaway_event = None def is_route_event_valid(self, route_event, time, sim, path): if not self.enabled: return False if self._safety_umbrella_putaway_event is route_event: location = path.final_location if is_location_outside(location.transform.translation, location.routing_surface.secondary_id): self._safety_umbrella_putaway_event = None return False elif not sims4.math.almost_equal(time, path.duration() - 0.5): self._safety_umbrella_putaway_event = None return False return True def _no_regular_put_away_scheduled(self, route_event_context): if self._safety_umbrella_putaway_event is None: return not route_event_context.route_event_already_scheduled( self.umbrella_route_events.exit_carry_event, provider=self) for route_event in route_event_context.route_event_of_data_type_gen( type(self._safety_umbrella_putaway_event.event_data)): if route_event is not self._safety_umbrella_putaway_event: return False return True def _has_routing_events(self): if self.umbrella_route_events is None or self.umbrella_route_events.enter_carry_event is None or self.umbrella_route_events.exit_carry_event is None: return False return True def provide_route_events(self, route_event_context, sim, path, start_time=0, end_time=0, **kwargs): if not self.enabled: return if not self._has_routing_events(): return resolver = SingleSimResolver(sim.sim_info) can_carry_umbrella = self.umbrella_route_events.enter_carry_event.test( resolver) added_enter_carry = False added_exit_carry = False is_prev_point_outside = None prev_time = None node = None prev_node = None prev_force_no_carry = False for (transform, routing_surface, time) in path.get_location_data_along_path_gen( time_step=0.5, start_time=start_time, end_time=end_time): force_no_carry = routing_surface.type == SurfaceType.SURFACETYPE_POOL if not force_no_carry: node = path.node_at_time(time) if node is prev_node: force_no_carry = prev_force_no_carry elif not force_no_carry: if node.portal_object_id != 0: portal_object = services.object_manager( routing_surface.primary_id).get( node.portal_object_id) if portal_object is not None: portal_instance = portal_object.get_portal_by_id( node.portal_id) force_no_carry = portal_instance is not None and ( portal_instance.portal_template is not None and (portal_instance.portal_template.required_flags is not None and portal_instance.portal_template.required_flags & PortalFlags.REQUIRE_NO_CARRY == PortalFlags.REQUIRE_NO_CARRY)) level = 0 if routing_surface is None else routing_surface.secondary_id is_curr_point_outside = is_location_outside( transform.translation, level) if is_prev_point_outside is None: is_prev_point_outside = is_curr_point_outside prev_time = time else: if can_carry_umbrella: if not added_enter_carry: if is_curr_point_outside: if not route_event_context.route_event_already_scheduled( self.umbrella_route_events. enter_carry_event, provider=self): route_event_context.add_route_event( RouteEventType.FIRST_OUTDOOR, self.umbrella_route_events. enter_carry_event(provider=self, time=time)) added_enter_carry = True if not added_exit_carry: if is_curr_point_outside or not is_prev_point_outside or force_no_carry: if self._no_regular_put_away_scheduled( route_event_context): route_event_context.add_route_event( RouteEventType.LAST_OUTDOOR, self.umbrella_route_events.exit_carry_event( provider=self, time=prev_time)) added_exit_carry = True if not can_carry_umbrella or added_enter_carry: if added_exit_carry: break is_prev_point_outside = is_curr_point_outside prev_time = time prev_node = node prev_force_no_carry = force_no_carry if self._safety_umbrella_putaway_event is None: location = path.final_location if not is_location_outside(location.transform.translation, location.routing_surface.secondary_id): self._safety_umbrella_putaway_event = self.umbrella_route_events.exit_carry_event( provider=self, time=path.duration() - 0.5) route_event_context.add_route_event( RouteEventType.LAST_OUTDOOR, self._safety_umbrella_putaway_event)
class _LoudNeighborState(CommonInteractionCompletedSituationState): LOOT_ACTION_DELAY = 'loot_action_delay' FACTORY_TUNABLES = {'loot_actions_on_situation_start': TunableTuple(description='\n A list of loot actions and a delay before they are applied to all\n instanced sims on the active lot.\n ', loot_actions=TunableList(description='\n Loot Actions that will be applied to instanced Sims on lot when\n this situation starts.\n ', tunable=LootActions.TunableReference(description='\n A loot action applied to instanced Sims on the active lot when\n the situation starts.\n ')), delay=TunableSimMinute(description="\n The delay in sim minutes before we give the loot to Sims on\n lot. This delay starts from when the loud state is set on the\n neighbor's door.\n ", default=0, minimum=0))} def __init__(self, *args, loot_actions_on_situation_start=None, **kwargs): super().__init__(*args, **kwargs) self._loot_actions_on_situation_start = loot_actions_on_situation_start def on_activate(self, reader=None): super().on_activate(reader) self._add_neighbor_to_auto_fill_blacklist() self._set_loud_door_state() self._create_or_load_alarm(_LoudNeighborState.LOOT_ACTION_DELAY, self._loot_actions_on_situation_start.delay, lambda _: self._apply_loud_loot_actions(), should_persist=True, reader=reader) def _add_neighbor_to_auto_fill_blacklist(self): timeout = self._get_remaining_alarm_time(self._time_out_string) next_state_timeout = self.owner.complain_state()._time_out if next_state_timeout is not None: timeout += clock.interval_in_sim_minutes(next_state_timeout) services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist(self.owner._neighbor_sim_id, blacklist_all_jobs_time=timeout.in_hours()) def _set_loud_door_state(self): if self.owner._neighbor_door_id is None: self.owner._self_destruct() apartment_door = services.object_manager().get(self.owner._neighbor_door_id) if apartment_door is None: self.owner._self_destruct() return apartment_door.set_state(self.owner.loud_door_state_on.state, self.owner.loud_door_state_on) def _apply_loud_loot_actions(self): sim_info_manager = services.sim_info_manager() for sim in sim_info_manager.instanced_sims_gen(): resolver = SingleSimResolver(sim.sim_info) if sim.is_on_active_lot(): for loot_action in self._loot_actions_on_situation_start.loot_actions: loot_action.apply_to_resolver(resolver) def _on_interaction_of_interest_complete(self, **kwargs): self.owner._change_state(self.owner.complain_state()) def timer_expired(self): services.get_zone_situation_manager().remove_sim_from_auto_fill_blacklist(self.owner._neighbor_sim_id) self.owner._self_destruct()
class TimedAspiration(TimedAspirationDefinitionDisplayMixin, AspirationBasic): INSTANCE_TUNABLES = { 'duration': TunableVariant( weekly_schedule=WeeklySchedule.TunableFactory( description= '\n Determines days of the week when the aspiration will deactivate.\n ' ), duration=TunableTimeSpan( description= '\n The time that this aspiration is active within the tracker.\n ' )), 'on_complete_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration complete.\n ', tunable=LootActions.TunableReference()), 'on_failure_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration fails.\n ', tunable=LootActions.TunableReference()), 'on_cancel_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration is cancelled.\n ', tunable=LootActions.TunableReference()), 'warning_buff': OptionalTunable( description= '\n If enabled, the buff is given to the Sim as a warning the aspiration\n duration is ending.\n ', tunable=TunableBuffReference( description= '\n The buff that is given to the Sim when the aspiration is getting\n close to timing out.\n ' )), 'tests': TunableTestSetWithTooltip( description= '\n Test set that must pass for this aspiration to be available.\n ' ) } @constproperty def aspiration_type(): return AspriationType.TIMED_ASPIRATION @classmethod def apply_on_complete_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_complete_loot_actions: loot_action.apply_to_resolver(resolver) @classmethod def apply_on_failure_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_failure_loot_actions: loot_action.apply_to_resolver(resolver) @classmethod def apply_on_cancel_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_cancel_loot_actions: loot_action.apply_to_resolver(resolver) def generate_aspiration_data(self, aspiration, **kwargs): return TimedAspirationData(self, aspiration)