class TunableRewardMoney(TunableRewardBase): FACTORY_TUNABLES = { 'money': TunableLiteralOrRandomValue( description= '\n Give money to a sim/household.\n ', tunable_type=int, default=10) } def __init__(self, *args, money, **kwargs): super().__init__(*args, **kwargs) self._awarded_money = money.random_int() @constproperty def reward_type(): return RewardType.MONEY def open_reward(self, sim_info, **kwargs): household = services.household_manager().get(sim_info.household_id) if household is not None: household.funds.add(self._awarded_money, Consts_pb2.TELEMETRY_MONEY_ASPIRATION_REWARD, sim_info.get_sim_instance()) def _get_display_text(self, resolver=None): return LocalizationHelperTuning.get_money(self._awarded_money)
class MoneyChange(BaseLootOperation): __qualname__ = 'MoneyChange' DISPLAY_TEXT = TunableLocalizedStringFactory( description= '\n A string displaying the Simoleon amount that this loot operation awards.\n It will be provided one token: the amount of Simoleons awarded.\n ' ) @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, value): if value._subject == interactions.ParticipantType.Invalid: logger.error("{} doesn't have a valid participant type tuned", source) FACTORY_TUNABLES = { 'amount': TunableLiteralOrRandomValue( description= '\n The amount of Simoleons awarded. The value will be rounded to the\n closest integer. When two integers are equally close, rounding is done\n towards the even one (e.g. 0.5 -> 0, 1.5 -> 2).\n ', tunable_type=float, minimum=0), 'statistic_multipliers': TunableList( description= '\n Tunables for adding statistic based multipliers to the payout in the\n format:\n \n amount *= statistic.value\n ', tunable=TunableStatisticModifierCurve.TunableFactory()), 'display_to_user': Tunable( description= '\n If true, the amount will be displayed in the interaction name.\n ', tunable_type=bool, needs_tuning=True, default=True), 'verify_tunable_callback': _verify_tunable_callback } def __init__(self, amount, statistic_multipliers, display_to_user, **kwargs): super().__init__(**kwargs) self._amount = amount self._statistic_multipliers = statistic_multipliers self._display_to_user = display_to_user self._random_amount = None @property def loot_type(self): return interactions.utils.LootType.SIMOLEONS def get_simoleon_delta(self, interaction, target=DEFAULT, context=DEFAULT): if not self._display_to_user: return 0 if not self._tests.run_tests( interaction.get_resolver(target=target, context=context)): return 0 sim = context.sim if context is not DEFAULT else DEFAULT recipients = interaction.get_participants( participant_type=self.subject, sim=sim, target=target) skill_multiplier = 1 if context is DEFAULT else interaction.get_skill_multiplier( interaction.monetary_payout_multipliers, context.sim) return self.amount * len(recipients) * skill_multiplier def _apply_to_subject_and_target(self, subject, target, resolver): interaction = resolver.interaction if interaction is not None: money_liability = interaction.get_liability( MoneyLiability.LIABILITY_TOKEN) if money_liability is None: money_liability = MoneyLiability() interaction.add_liability(MoneyLiability.LIABILITY_TOKEN, money_liability) skill_multiplier = interaction.get_skill_multiplier( interaction.monetary_payout_multipliers, interaction.sim) else: money_liability = None skill_multiplier = 1 subject_obj = self._get_object_from_recipient(subject) amount_multiplier = self._get_multiplier( resolver, subject_obj) * skill_multiplier amount = round(self.amount * amount_multiplier) if amount: if money_liability is not None: money_liability.amounts[self.subject] += amount if interaction is not None: interaction_category_tags = interaction.interaction_category_tags else: interaction_category_tags = None subject.household.funds.add( amount, Consts_pb2.TELEMETRY_INTERACTION_REWARD, subject_obj, tags=interaction_category_tags) def _on_apply_completed(self): self._random_amount = None def _get_display_text(self): return self.DISPLAY_TEXT(*self._get_display_text_tokens()) def _get_display_text_tokens(self): return (self.amount, ) def _get_multiplier(self, resolver, sim): amount_multiplier = 1 if self._statistic_multipliers: for statistic_multiplier in self._statistic_multipliers: amount_multiplier *= statistic_multiplier.get_multiplier( resolver, sim) return amount_multiplier @property def amount(self): if self._random_amount is None: self._random_amount = self._amount.random_float() return self._random_amount
class Topic(metaclass=TunedInstanceMetaclass, manager=services.topic_manager()): __qualname__ = 'Topic' INSTANCE_TUNABLES = { 'score_bonus': Tunable( description= '\n Score bonus for matching topic tag.\n ', tunable_type=int, default=0), 'guaranteed_content': OptionalTunable( TunableTuple( description= '\n If enabled, will force content set generation to add options for\n this topic.\n ', count=Tunable( description= '\n The number of options to force into the content set.\n ', tunable_type=int, default=1), priority=Tunable( description= '\n The priority of this Topic vs. other Topics. Ties are randomized.\n ', tunable_type=int, default=0))), 'relevancy_value': TunableLiteralOrRandomValue( description= '\n Initial Decay value once value has reached zero topic will be\n removed. If is_timeout is set, this will the number of minutes\n before topic will timeout.\n ', tunable_type=int, default=1), 'is_timed_relevancy': Tunable( description= '\n If set, relevancy value is treated as number of minutes until topic\n is removed.\n ', tunable_type=bool, default=False) } @classmethod def topic_exist_in_sim(cls, sim, target=None): return sim.has_topic(cls, target=target) @classmethod def score_for_sim(cls, sim, target=None): if cls.topic_exist_in_sim(sim, target): return cls.score_bonus return 0 def __init__(self, target): def on_target_deleted(ref): self.is_valid = False self._target_ref = target.ref( on_target_deleted) if target is not None else None self.reset_relevancy() self.is_valid = True def reset_relevancy(self): relevancy = self.relevancy_value.random_int() if self.is_timed_relevancy: self.current_relevancy = services.time_service( ).sim_now + clock.interval_in_sim_minutes(relevancy) else: self.current_relevancy = relevancy def decay_topic(self, time): if not self.is_valid: return True if self.is_timed_relevancy: return time >= self.current_relevancy return self.current_relevancy <= 0 def target_matches(self, target): return self.is_valid and target is self.target @property def target(self): if self._target_ref is not None: return self._target_ref()
class LifeSkillStatistic(HasTunableReference, LifeSkillDisplayMixin, TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)): REMOVE_INSTANCE_TUNABLES = ('initial_value', ) INSTANCE_TUNABLES = { 'min_value_tuning': Tunable(description= '\n The minimum value for this stat.\n ', tunable_type=float, default=-100, export_modes=ExportModes.All), 'max_value_tuning': Tunable(description= '\n The maximum value for this stat.\n ', tunable_type=float, default=100, export_modes=ExportModes.All), 'initial_tuning': TunableLiteralOrRandomValue( description= '\n The initial value of this stat. Can be a single value or range.\n ', tunable_type=float, default=0, minimum=-100), 'initial_test_based_modifiers': TunableList( description= '\n List of tuples containing test and a random value. If the test passes,\n a random value is added to the already random initial value. \n ', tunable=TunableTuple( description= '\n A container for test and the corresponding random value.\n ', initial_value_test=TunableTestSet( description= '\n If test passes, then the random value tuned will be applied\n to the initial value. \n ' ), initial_modified_value=TunableLiteralOrRandomValue( description= '\n The initial value of this stat. Can be a single value or range.\n ', tunable_type=float, default=0, minimum=-100))), 'age_to_remove_stat': TunableEnumEntry( description= '\n When sim reaches this age, this stat will be removed permanently. \n ', tunable_type=Age, default=Age.YOUNGADULT), 'missing_career_decay_rate': Tunable( description= '\n How much this life skill decay by if sim is late for school/work.\n ', tunable_type=float, default=0.0), 'trait_on_age_up_list': TunableList( description= '\n A list of trait that will be applied on age up if this commodity \n falls within the range specified in this tuple.\n It also contains other visual information like VFX and notification.\n ', tunable=TunableTuple( description= '\n A container for the range and corresponding information.\n ', export_class_name='TunableTraitOnAgeUpTuple', life_skill_range=TunableInterval( description= '\n If the commodity is in this range on age up, the trait\n will be applied. \n The vfx and notification will be played every time the \n range is crossed.\n ', tunable_type=float, default_lower=0, default_upper=100, export_modes=ExportModes.All), age_up_info=OptionalTunable( description= "\n If enabled, this trait will be added on age up given the specified age. \n Otherwise, no trait will be added.\n We don't use loot because UI needs this trait exported for display.\n ", enabled_name='enabled_age_up_info', tunable=TunableTuple( export_class_name='TunableAgeUpInfoTuple', age_to_apply_trait=TunableEnumEntry( description= '\n When sim reaches this age, this trait will be added on age up.\n ', tunable_type=Age, default=Age.YOUNGADULT), life_skill_trait=Trait.TunableReference( description= '\n Trait that is added on age up.\n ', pack_safe=True)), export_modes=ExportModes.All), in_range_notification= OptionalTunable(tunable=TunableUiDialogNotificationSnippet( description= '\n Notification that is sent when the commodity reaches this range.\n ' )), out_of_range_notification= OptionalTunable(tunable=TunableUiDialogNotificationSnippet( description= '\n Notification that is sent when the commodity exits this range.\n ' )), vfx_triggered=TunablePlayEffectVariant( description= '\n Vfx to play on the sim when commodity enters this threshold.\n ', tuning_group=GroupNames.ANIMATION), in_range_buff=OptionalTunable(tunable=TunableBuffReference( description= '\n Buff that is added when sim enters this threshold.\n ' )))), 'headline': TunableReference( description= '\n The headline that we want to send down when this life skill updates.\n ', manager=services.get_instance_manager( sims4.resources.Types.HEADLINE), tuning_group=GroupNames.UI) } def __init__(self, tracker): self._vfx = None super().__init__(tracker, self.get_initial_value()) self._last_update_value = None if not tracker.load_in_progress: self._apply_initial_value_modifier() @classproperty def persists_across_gallery_for_state(cls): if cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_FOR_ALL or cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_ONLY_FOR_OBJECT: return True return False @classmethod def get_initial_value(cls): return cls.initial_tuning.random_int() def _apply_initial_value_modifier(self): initial_value = self._value resolver = SingleSimResolver(self.tracker.owner) for initial_modifier in self.initial_test_based_modifiers: if initial_modifier.initial_value_test.run_tests(resolver): initial_value += initial_modifier.initial_modified_value.random_float( ) self.set_value(initial_value, from_add=True) def _update_value(self): old_value = self._value super()._update_value() new_value = self._value self._evaluate_threshold(old_value=old_value, new_value=new_value) def _evaluate_threshold(self, old_value=0, new_value=0, from_load=False): old_infos = [] new_infos = [] for range_info in self.trait_on_age_up_list: if old_value in range_info.life_skill_range: old_infos.append(range_info) if new_value in range_info.life_skill_range: new_infos.append(range_info) old_infos_set = set(old_infos) new_infos_set = set(new_infos) out_ranges = old_infos_set - new_infos_set in_ranges = new_infos_set - old_infos_set owner = self.tracker.owner is_household_sim = owner.is_selectable and owner.valid_for_distribution if not from_load: for out_range in out_ranges: if out_range.out_of_range_notification is not None and is_household_sim: dialog = out_range.out_of_range_notification( owner, resolver=SingleSimResolver(owner)) dialog.show_dialog(additional_tokens=(owner, )) if out_range.in_range_buff is not None: owner.Buffs.remove_buff_by_type( out_range.in_range_buff.buff_type) for in_range in in_ranges: if in_range.in_range_notification is not None and not from_load and is_household_sim: dialog = in_range.in_range_notification( owner, resolver=SingleSimResolver(owner)) dialog.show_dialog(additional_tokens=(owner, )) if in_range.vfx_triggered is not None and not from_load and is_household_sim: if self._vfx is not None: self._vfx.stop(immediate=True) self._vfx = None sim = owner.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: self._vfx = in_range.vfx_triggered(sim) self._vfx.start() if in_range.in_range_buff is not None: owner.Buffs.add_buff( in_range.in_range_buff.buff_type, buff_reason=in_range.in_range_buff.buff_reason) def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) self.create_and_send_commodity_update_msg(is_rate_change=False) @constproperty def remove_on_convergence(): return False def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self._value super().set_value(value, *args, from_load=from_load, interaction=interaction, **kwargs) new_value = self._value self._evaluate_threshold(old_value=old_value, new_value=new_value, from_load=from_load) if from_load: return self.create_and_send_commodity_update_msg(is_rate_change=False, from_add=kwargs.get( 'from_add', False)) def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) if self._vfx is not None: self._vfx.stop(immediate=True) self._vfx = None def save_statistic(self, commodities, skills, ranked_statistics, tracker): message = protocols.Commodity() message.name_hash = self.guid64 message.value = self.get_saved_value() if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks( ) commodities.append(message) def create_and_send_commodity_update_msg(self, is_rate_change=True, allow_npc=False, from_add=False): current_value = self.get_value() change_rate = self.get_change_rate() life_skill_msg = Commodities_pb2.LifeSkillUpdate() life_skill_msg.sim_id = self.tracker.owner.id life_skill_msg.life_skill_id = self.guid64 life_skill_msg.curr_value = current_value life_skill_msg.rate_of_change = change_rate life_skill_msg.is_from_add = from_add send_sim_life_skill_update_message(self.tracker.owner, life_skill_msg) if self._last_update_value is None: value_to_send = change_rate else: value_to_send = current_value - self._last_update_value self._last_update_value = current_value if value_to_send != 0 and not from_add: self.headline.send_headline_message(self.tracker.owner, value_to_send) def create_and_send_life_skill_delete_msg(self): life_skill_msg = Commodities_pb2.LifeSkillDelete() life_skill_msg.sim_id = self.tracker.owner.id life_skill_msg.life_skill_id = self.guid64 send_sim_life_skill_delete_message(self.tracker.owner, life_skill_msg)
class StoryProgressionDestinationPopulateAction(_StoryProgressionAction): FACTORY_TUNABLES = { '_region_to_rentable_zone_density': TunableMapping( description= '\n Based on region what percent of available lots will be filled.\n ', key_name='Region Description', key_type=TunableRegionDescription(pack_safe=True), value_name='Rentable Zone Density', value_type=TunableTuple( _venues_to_populate=TunableSet( description= '\n A set of venue references that are considered to be rentable.\n ', tunable=TunableReference( manager=services. get_instance_manager( sims4.resources. Types.VENUE), pack_safe =True)), household_description_to_ideal_travel_group_size=TunableMapping( description= '\n Based on the house description how many sims should go on vacation\n ', key_name='House Description', key_type=TunableHouseDescription(pack_safe=True), value_name='Travel Group Size', value_type=TunableLiteralOrRandomValue( description= '\n The maximum number of sims that should go on vacation to\n that lot.\n ', tunable_type=int, minimum=0)), bed_count_to_travel_group_size=TunableMapping( description= '\n Based on the house description how many sims should go on vacation\n ', key_name='Number of beds', key_type=Tunable( description= '\n The number of beds on the lot to determine how many sims\n can go in the vacation group.\n ', tunable_type=int, default=1), value_name='Travel Group Size', value_type=TunableLiteralOrRandomValue( description= '\n The maximum number of sims that should go on vacation to\n that lot.\n ', tunable_type=int, minimum=0)), travel_group_size_to_household_template=TunableMapping( description= '\n Mapping to travel group size to household templates. If there\n are no household that fulfill the requirement of renting a\n zone, then random household template will chosen to be created\n to rent a zone.\n ', key_type=Tunable(tunable_type=int, default=1), value_type=TunableList( description= '\n Household template that will be created for renting a zone.\n ', tunable=HouseholdTemplate.TunableReference())), density=TunablePercent( description= '\n Percent of lots will be occupied once a user sim has rented a lot.\n ', default=80), min_to_populate=TunableRange( description= '\n Minimum number of lots that should be rented.\n ', tunable_type=int, default=3, minimum=0), duration=TunableLiteralOrRandomValue( description= "\n The maximum in sim days npc's should stay on vacation.\n ", tunable_type=int, minimum=1, default=1))) } def should_process(self, options): if services.active_household_id() == 0: return False return True def _get_rentable_zones(self, rentable_zone_density_data, neighborhood_proto_buff): num_zones_rented = 0 available_zone_ids = [] travel_group_manager = services.travel_group_manager() venue_manager = services.get_instance_manager( sims4.resources.Types.VENUE) for lot_owner_info in neighborhood_proto_buff.lots: if lot_owner_info.venue_key == 0: continue if venue_manager.get( lot_owner_info.venue_key ) not in rentable_zone_density_data._venues_to_populate: continue zone_id = lot_owner_info.zone_instance_id if not travel_group_manager.is_zone_rentable(zone_id): num_zones_rented += 1 else: available_zone_ids.append(zone_id) return (num_zones_rented, available_zone_ids) def process_action(self, story_progression_flags): zone = services.current_zone() neighborhood_proto = services.get_persistence_service( ).get_neighborhood_proto_buff(zone.neighborhood_id) region_id = neighborhood_proto.region_id rentable_zone_density_data = self._region_to_rentable_zone_density.get( region_id) if rentable_zone_density_data is None: return (num_zones_rented, available_zone_ids) = self._get_rentable_zones( rentable_zone_density_data, neighborhood_proto) num_available_zone_ids = len(available_zone_ids) if num_available_zone_ids == 0: return number_zones_to_fill = 0 num_desired_zones_filled = math.floor( (num_zones_rented + num_available_zone_ids) * rentable_zone_density_data.density) if num_desired_zones_filled < rentable_zone_density_data.min_to_populate: num_desired_zones_filled = rentable_zone_density_data.min_to_populate if num_zones_rented < num_desired_zones_filled: number_zones_to_fill = num_desired_zones_filled - num_zones_rented neighborhood_population_service = services.neighborhood_population_service( ) if neighborhood_population_service is None: return neighborhood_population_service.add_rentable_lot_request( number_zones_to_fill, zone.neighborhood_id, None, available_zone_ids, rentable_zone_density_data)
class _DesiredSituations(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'desired_sim_count': TunableLiteralOrRandomValue( description= '\n The number of Sims desired to be participating in the situation.\n ', tunable_type=int, default=0), 'disable_churn': Tunable( description= "\n If checked, we disable churn for this shift change. That means we\n only fire the situation on shift change, not in between shifts. So\n if you have a situation in this shift and it ends, we don't spin up\n another one on the next churn (based on churn interval). Basically\n means you want a one shot situation, fire and forget.\n \n If unchecked, we will try to maintain the desired number of\n situations at every churn interval during this shift change.\n ", tunable_type=bool, default=False) } @TunableFactory.factory_option def get_create_params(user_facing=False): create_params = {} create_params_locked = {} if user_facing: create_params['user_facing'] = Tunable( description= "\n If enabled, we will start the situation as user facing.\n Note: We can only have one user facing situation at a time,\n so make sure you aren't tuning multiple user facing\n situations to occur at once.\n ", tunable_type=bool, default=False) else: create_params_locked['user_facing'] = False return { 'weighted_situations': TunableList( description= '\n A weighted list of situations to be used while fulfilling the\n desired Sim count.\n ', tunable=TunableTuple( situation=Situation.TunableReference(pack_safe=True), params=TunableTuple( description= '\n Situation creation parameters.\n ', locked_args=create_params_locked, **create_params), weight=Tunable(tunable_type=int, default=1), weight_multipliers=TunableMultiplier.TunableFactory( description= "\n Tunable tested multiplier to apply to weight.\n \n *IMPORTANT* The only participants that work are ones\n available globally, such as Lot and ActiveHousehold. Only\n use these participant types or use tests that don't rely\n on any, such as testing all objects via Object Criteria\n test or testing active zone with the Zone test.\n ", locked_args={'base_value': 1}), tests=TunableTestSet( description= "\n A set of tests that must pass for the situation and weight\n pair to be available for selection.\n \n *IMPORTANT* The only participants that work are ones\n available globally, such as Lot and ActiveHousehold. Only\n use these participant types or use tests that don't rely\n on any, such as testing all objects via Object Criteria\n test or testing active zone with the Zone test.\n " ))) } def get_weighted_situations(self, predicate=lambda _: True): resolver = GlobalResolver() def get_weight(item): if not predicate(item.situation): return 0 if not item.tests.run_tests(resolver): return 0 return item.weight * item.weight_multipliers.get_multiplier( resolver) * item.situation.weight_multipliers.get_multiplier( resolver) weighted_situations = tuple( (get_weight(item), (item.situation, dict(item.params.items()))) for item in self.weighted_situations) return weighted_situations def get_situation_and_params(self, predicate=lambda _: True, additional_situations=None): weighted_situations = self.get_weighted_situations(predicate=predicate) if additional_situations is not None: weighted_situations = tuple(weighted_situations) + tuple( additional_situations) situation_and_params = random.weighted_random_item(weighted_situations) if situation_and_params is not None: return situation_and_params return (None, {})
class SlotItemTransfer(XevtTriggeredElement, HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'objects_to_transfer': TunableObjectGeneratorVariant( description= "\n The objects whose slots will be checked for objects to be gathered\n into the Sim's inventory.\n ", participant_default=ParticipantType.Object), 'object_tests': TunableTestSet( description= '\n Tests that will run on each object, and will harvest the object\n only if all the tests pass.\n \n The object will be the PickedObject participant type, so we can\n preserve the interaction resolver.\n ' ), 'fallback_to_household_inventory': Tunable( description= "\n If enabled, and we fail to add the object to the Sim's inventory,\n we will attempt to add it to the household inventory.\n ", tunable_type=bool, default=False), 'transfer_extra_objects': OptionalTunable( description= "\n If enabled, extra slot items will be transferred to the Sim's inventory\n from each object we gathered.\n \n For example: If we gathered 3 apple trees, and set count as 2, then we\n will harvest 3 * 2 = 6 extra apples from this interaction\n ", tunable=TunableTuple( count=TunableLiteralOrRandomValue( description= '\n The number of extra items to be transferred.\n ', tunable_type=int, default=1, minimum=0, maximum=10), tests=TunableTestSet( description= '\n A set of tests that must pass in order for this extra transfer\n to be applied.\n ' ))), 'use_gardening_optimization': Tunable( description= '\n If enabled, we will precompact the objects prior to adding them\n to inventory if the tuning ID and quality are the same regardless\n of any other differences.\n ', tunable_type=bool, default=True) } def _do_behavior(self): objects = self.objects_to_transfer.get_objects(self.interaction) if not objects: return False transfer_extra = self.transfer_extra_objects should_transfer_extra = False if transfer_extra: resolver = self.interaction.get_resolver() if transfer_extra.tests.run_tests(resolver): should_transfer_extra = True objects_to_transfer = [] num_extra_objects = 0 for obj in objects: has_extra_to_transfer = False for child_inst in obj.children: interaction_parameters = {'picked_item_ids': (child_inst.id, )} resolver = self.interaction.get_resolver( **interaction_parameters) if self.object_tests.run_tests(resolver): objects_to_transfer.append(child_inst) has_extra_to_transfer = True if should_transfer_extra: if has_extra_to_transfer: num_extra_objects += transfer_extra.count.random_int() stacked_objects = self._stack_objects_transfer(objects_to_transfer, num_extra_objects) sim = self.interaction.sim sim_inventory = sim.inventory_component for (obj, _) in stacked_objects: obj.update_ownership(sim) if sim_inventory.can_add(obj): if obj.live_drag_component is not None: obj.live_drag_component.resolve_live_drag_household_permission( ) sim_inventory.player_try_add_object(obj) elif self.fallback_to_household_inventory: build_buy.move_object_to_household_inventory(obj) def _stack_objects_transfer(self, objects_to_harvest, num_extra_objects): obj_count = {} dupe_objects = [] if self.use_gardening_optimization: unique_objects = [] while objects_to_harvest: obj = objects_to_harvest.pop() quality_value = obj.get_state( GardeningTuning.QUALITY_STATE_VALUE) if obj.has_state( GardeningTuning.QUALITY_STATE_VALUE) else 0 object_count_key = (obj.guid64, quality_value) curr_count = obj_count.get(object_count_key, 0) if curr_count == 0: unique_objects.append((obj, quality_value)) else: dupe_objects.append(obj) curr_count = curr_count + 1 obj_count[object_count_key] = curr_count for (obj, quality_value) in unique_objects: object_count_key = (obj.guid64, quality_value) curr_count = obj_count.get(object_count_key, 0) obj.set_stack_count(curr_count) else: unique_objects = [(obj, 0) for obj in objects_to_harvest] if unique_objects: while num_extra_objects > 0: (obj, _) = random.choice(unique_objects) obj.update_stack_count(1) num_extra_objects -= 1 if dupe_objects: services.get_reset_and_delete_service().trigger_batch_destroy( dupe_objects) return unique_objects
class AgingTransition(HasTunableSingletonFactory, AutoFactoryInit): class _AgeTransitionShowDialog(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'dialog': SimPersonalityAssignmentDialog.TunableFactory(locked_args={'phone_ring_type': PhoneRingType.NO_RING})} def __call__(self, sim_info, **kwargs): def on_response(dlg): if dlg.accepted: sim_info.resend_trait_ids() dialog = self.dialog(sim_info, assignment_sim_info=sim_info, resolver=SingleSimResolver(sim_info)) dialog.show_dialog(on_response=on_response, **kwargs) class _AgeTransitionShowNotification(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'dialog': UiDialogNotification.TunableFactory()} def __call__(self, sim_info, **__): dialog = self.dialog(sim_info, resolver=SingleSimResolver(sim_info)) dialog.show_dialog() FACTORY_TUNABLES = {'age_up_warning_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n Notification to show up when Age Up is impending.\n ', tuning_group=GroupNames.UI)), 'age_up_available_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n Notification to show when Age Up is ready.\n ', tuning_group=GroupNames.UI)), '_age_duration': TunableLiteralOrRandomValue(description="\n The time, in Sim days, required for a Sim to be eligible to\n transition from this age to the next one.\n \n If a random range is specified, the random value will be seeded on\n the Sim's ID.\n ", tunable_type=float, default=1), '_use_initial_age_randomization': Tunable(description="\n If checked, instead of randomizing the duration of each individual age,\n the sims's initial age progress will be randomly offset on first load. \n ", tunable_type=bool, default=False), 'age_transition_warning': Tunable(description='\n Number of Sim days prior to the transition a Sim will get a warning\n of impending new age.\n ', tunable_type=float, default=1), 'age_transition_delay': Tunable(description='\n Number of Sim days after transition time elapsed before auto- aging\n occurs.\n ', tunable_type=float, default=1), 'age_transition_dialog': TunableVariant(description='\n The dialog or notification that is displayed when the Sim ages up.\n ', show_dialog=_AgeTransitionShowDialog.TunableFactory(), show_notification=_AgeTransitionShowNotification.TunableFactory(), locked_args={'no_dialog': None}, default='no_dialog', tuning_group=GroupNames.UI), 'age_trait': TunableReference(description="\n The age trait that corresponds to this Sim's age\n ", manager=services.get_instance_manager(sims4.resources.Types.TRAIT)), 'relbit_based_loot': TunableList(description='\n List of loots given based on bits set in existing relationships.\n Applied after per household member loot.\n ', tunable=TunableTuple(description='\n Loot given to sim aging up (actor) and each sim (target) with a\n "chained" relationship via recursing through the list of relbit\n sets.\n ', relationship=TunableList(description='\n List specifying a series of relationship(s) to recursively \n traverse to find the desired target sim. i.e. to find \n "cousins", we get all the "parents". And then we \n get "aunts/uncles" by getting the "siblings" of those \n "parents". And then we finally get the "cousins" by \n getting the "children" of those "aunts/uncles".\n \n So:\n Set of "parent" bitflag(s)\n Set of "sibling" bitflag(s)\n Set of "children" bitflag(s)\n \n Can also find direct existing relationships by only having a\n single entry in the list.\n ', tunable=TunableSet(description='\n Set of relbits to use for this relationship.\n ', tunable=RelationshipBit.TunableReference(description='\n The relationship bit between greeted Sims.\n ', pack_safe=True))), loot=TunableList(description='\n Loot given between sim aging up and sims with the previously\n specified chain of relbits. (may create a relationship).\n ', tunable=LootActions.TunableReference(description='\n A loot action given to sim aging up.\n ', pack_safe=True))), tuning_group=GroupNames.TRIGGERS), 'per_household_member_loot': TunableList(description="\n Loots given between sim aging up (actor) and each sim in that sims\n household (target). Applied before relbit based loot'\n ", tunable=LootActions.TunableReference(description='\n A loot action given between sim aging up (actor) and each sim in\n that sims household (target).\n ', pack_safe=True), tuning_group=GroupNames.TRIGGERS), 'single_sim_loot': TunableList(description='\n Loots given to sim aging up (actor). Last loot applied.\n ', tunable=LootActions.TunableReference(description='\n A loot action given to sim aging up.\n ', pack_safe=True), tuning_group=GroupNames.TRIGGERS)} def get_age_duration(self, sim_info): if self._use_initial_age_randomization: return (self._age_duration.upper_bound + self._age_duration.lower_bound)/2 return self._get_random_age_duration(sim_info) def _get_random_age_duration(self, sim_info): return self._age_duration.random_float(seed=(self.age_trait.guid64, sim_info.sim_id)) def get_randomized_progress(self, sim_info, age_progress): if self._use_initial_age_randomization: previous_age_duration = self._get_random_age_duration(sim_info) current_age_duration = self.get_age_duration(sim_info) if sims4.math.almost_equal(previous_age_duration, current_age_duration): return age_progress age_progress = current_age_duration + age_progress - previous_age_duration if age_progress < 0: age_progress = self._age_duration.upper_bound - self._age_duration.lower_bound + age_progress return age_progress def _apply_aging_transition_relbit_loot(self, source_info, cur_info, relbit_based_loot, level): if level == len(relbit_based_loot.relationship): resolver = DoubleSimResolver(source_info, cur_info) for loot in relbit_based_loot.loot: loot.apply_to_resolver(resolver) return relationship_tracker = cur_info.relationship_tracker for target_sim_id in relationship_tracker.target_sim_gen(): if set(relationship_tracker.get_all_bits(target_sim_id)) & relbit_based_loot.relationship[level]: new_sim_info = services.sim_info_manager().get(target_sim_id) self._apply_aging_transition_relbit_loot(source_info, new_sim_info, relbit_based_loot, level + 1) def apply_aging_transition_loot(self, sim_info): if self.per_household_member_loot: for member_info in sim_info.household.sim_info_gen(): if member_info is sim_info: continue resolver = DoubleSimResolver(sim_info, member_info) for household_loot in self.per_household_member_loot: household_loot.apply_to_resolver(resolver) for relbit_based_loot in self.relbit_based_loot: self._apply_aging_transition_relbit_loot(sim_info, sim_info, relbit_based_loot, 0) resolver = SingleSimResolver(sim_info) for loot in self.single_sim_loot: loot.apply_to_resolver(resolver) def show_age_transition_dialog(self, sim_info, **kwargs): if self.age_transition_dialog is not None: self.age_transition_dialog(sim_info, **kwargs)
class MoneyChange(BaseLootOperation): FACTORY_TUNABLES = { 'amount': TunableLiteralOrRandomValue( description= '\n The amount of Simoleons awarded. The value will be rounded to the\n closest integer. When two integers are equally close, rounding is done\n towards the even one (e.g. 0.5 -> 0, 1.5 -> 2). Negative amounts allowed\n and allow partial deductions (will only take balance to zero, not negative).\n ', tunable_type=float, default=0, minimum=None), 'statistic_multipliers': TunableList( description= '\n Tunables for adding statistic based multipliers to the payout in the\n format:\n \n amount *= statistic.value\n ', tunable=TunableStatisticModifierCurve.TunableFactory()), 'display_to_user': Tunable( description= '\n If true, the amount will be displayed in the interaction name.\n ', tunable_type=bool, default=False), 'notification': OptionalTunable( description= '\n If set and an amount is awarded, displays a dialog to the user.\n \n The notification will have access to the amount awarded as a localization token. e.g. {0.Money} \n ', tunable=TunableUiDialogNotificationSnippet()) } def __init__(self, amount, statistic_multipliers, display_to_user, notification, **kwargs): super().__init__(**kwargs) self._amount = amount self._statistic_multipliers = statistic_multipliers self._display_to_user = display_to_user self._random_amount = None self._notification = notification @property def loot_type(self): return interactions.utils.LootType.SIMOLEONS def get_simoleon_delta(self, interaction, target=DEFAULT, context=DEFAULT, **interaction_parameters): if not self._display_to_user: return (0, FundsSource.HOUSEHOLD) if not self._tests.run_tests( interaction.get_resolver( target=target, context=context, **interaction_parameters)): return (0, FundsSource.HOUSEHOLD) sim = context.sim if context is not DEFAULT else DEFAULT recipients = interaction.get_participants( participant_type=self.subject, sim=sim, target=target, **interaction_parameters) skill_multiplier = 1 if context is DEFAULT else interaction.get_skill_multiplier( interaction.monetary_payout_multipliers, context.sim) return (self.amount * len(recipients) * skill_multiplier, FundsSource.HOUSEHOLD) def _apply_to_subject_and_target(self, subject, target, resolver): interaction = resolver.interaction if interaction is not None: money_liability = interaction.get_liability( MoneyLiability.LIABILITY_TOKEN) if money_liability is None: money_liability = MoneyLiability() interaction.add_liability(MoneyLiability.LIABILITY_TOKEN, money_liability) skill_multiplier = interaction.get_skill_multiplier( interaction.monetary_payout_multipliers, interaction.sim) else: money_liability = None skill_multiplier = 1 subject_obj = self._get_object_from_recipient(subject) amount_multiplier = self._get_multiplier( resolver, subject_obj) * skill_multiplier amount = round(self.amount * amount_multiplier) if amount: if money_liability is not None: money_liability.amounts[self.subject] += amount if interaction is not None: interaction_category_tags = interaction.interaction_category_tags else: interaction_category_tags = None if amount < 0: subject.household.funds.try_remove_amount( -amount, Consts_pb2.TELEMETRY_INTERACTION_REWARD, subject_obj, require_full_amount=False) else: subject.household.funds.add( amount, Consts_pb2.TELEMETRY_INTERACTION_REWARD, subject_obj, tags=interaction_category_tags) if self._notification is not None: dialog = self._notification(subject, resolver=resolver) dialog.show_dialog(additional_tokens=(amount, )) def _on_apply_completed(self): self._random_amount = None def _get_display_text(self, resolver=None): return LocalizationHelperTuning.MONEY(*self._get_display_text_tokens()) def _get_display_text_tokens(self, resolver=None): return (self.amount, ) def _get_multiplier(self, resolver, sim): amount_multiplier = 1 if self._statistic_multipliers: for statistic_multiplier in self._statistic_multipliers: amount_multiplier *= statistic_multiplier.get_multiplier( resolver, sim) return amount_multiplier @property def amount(self): if self._random_amount is None: self._random_amount = self._amount.random_float() return self._random_amount