class LamarkianInheritance(_Inheritance): __qualname__ = 'GardeningTuning.LamarkianInheritance' CALCULATIONS = { ParentalContribution.MAXIMUM: max, ParentalContribution.MINIMUM: min, ParentalContribution.AVERAGE: lambda l: sum(l) / len(l) } FACTORY_TUNABLES = { '_calculation': TunableEnumEntry(ParentalContribution, ParentalContribution.MINIMUM, needs_tuning=True), 'fitness_stat': Statistic.TunableReference( description= '\n This statistic is used as the x-value on the fitness curve.\n ' ), 'fitness_curve_maximum': TunableCurve( description= '\n This curve maps fitness stat values to maximum state changes.\n ' ), 'fitness_curve_minimum': TunableCurve( description= '\n This curve maps fitness stat values to minimum state changes.\n ' ) } @property def calculation(self): return self.CALCULATIONS[self._calculation] def get_inherited_value(self, mother, father): values = [] indeces = [] if self.inherit_from_mother: values.append(mother.get_stat_value(self.fitness_stat)) inherited_state_value = mother.get_state(self.inherited_state) indeces.append( self.inherited_state.values.index(inherited_state_value)) if self.inherit_from_father: values.append(father.get_stat_value(self.fitness_stat)) inherited_state_value = father.get_state(self.inherited_state) indeces.append( self.inherited_state.values.index(inherited_state_value)) fitness_value = self.calculation(values) max_delta = self.fitness_curve_maximum.get(fitness_value) min_delta = self.fitness_curve_minimum.get(fitness_value) delta = round(sims4.random.uniform(min_delta, max_delta)) index = self.calculation(indeces) + delta index = sims4.math.clamp(0, index, len(self.inherited_state.values)) return self.inherited_state.values[index]
def __init__(self, **kwargs): super().__init__( description= '\n A tunable that increments a specified statistic by a specified\n amount, runs a sequence, and then decrements the statistic by the\n same amount.\n ', stat=Statistic.TunableReference( description= '\n The statistic to increment and decrement.\n ' ), subject=TunableEnumFlags( description= '\n The participant of the interaction on which the statistic will\n be incremented and decremented.\n ', enum_type=ParticipantType, default=ParticipantType.Object), transfer_stat=Statistic.TunableReference( description= '\n The statistic whose value to transfer.\n ' ), transfer_subject=TunableEnumFlags( description= '\n The participant of the interaction whose statistic value will\n be transferred.\n ', enum_type=ParticipantType, default=ParticipantType.Actor), **kwargs)
class GameRules(metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.GAME_RULESET)): __qualname__ = 'GameRules' INSTANCE_TUNABLES = { 'game_name': TunableLocalizedStringFactory( description='\n Name of the game.\n ', default=1860708663), 'teams_per_game': TunableInterval( description= '\n An interval specifying the number of teams allowed per game.\n \n Joining Sims are put on a new team if the maximum number of teams\n has not yet been met, otherwise they are put into the team with the\n fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_game': TunableInterval( description= '\n An interval specifying the number of players allowed per game.\n \n If the maximum number of players has not been met, Sims can\n continue to join a game. Joining Sims are put on a new team if the\n maximum number of teams as specified in the "teams_per_game"\n tunable has not yet been met, otherwise they are put into the team\n with the fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_turn': TunableRange( description= '\n An integer specifying number of players from the active team who\n take their turn at one time.\n ', tunable_type=int, default=1, minimum=1), 'initial_state': ObjectStateValue.TunableReference( description= "\n The game's starting object state.\n "), 'score_info': TunableTuple( description= "\n Tunables that affect the game's score.\n ", winning_score=Tunable( description= '\n An integer value specifying at what score the game will end.\n ', tunable_type=int, default=100), score_increase=TunableInterval( description= '\n An interval specifying the minimum and maximum score increases\n possible in one turn. A random value in this interval will be\n generated each time score loot is given.\n ', tunable_type=int, default_lower=35, default_upper=50, minimum=0), skill_level_bonus=Tunable( description= "\n A bonus number of points based on the Sim's skill level in the\n relevant_skill tunable that will be added to score_increase.\n \n ex: If this value is 2 and the Sim receiving score has a\n relevant skill level of 4, they will receive 8 (2 * 4) extra\n points.\n ", tunable_type=float, default=2), relevant_skill=Skill.TunableReference( description= "\n The skill relevant to this game. Each Sim's proficiency in\n this skill will effect the score increase they get.\n " ), use_effective_skill_level=Tunable( description= '\n If checked, we will use the effective skill level rather than\n the actual skill level of the relevant_skill tunable.\n ', tunable_type=bool, default=True), progress_stat=Statistic.TunableReference( description= '\n The statistic that advances the progress state of this game.\n ' )), 'clear_score_on_player_join': Tunable( description= '\n Tunable that, when checked, will clear the game score when a player joins.\n \n This essentially resets the game.\n ', tunable_type=bool, default=False), 'alternate_target_object': OptionalTunable( description= '\n Tunable that, when enabled, means the game should create an alternate object\n in the specified slot on setup that will be modified as the game goes on\n and destroyed when the game ends.\n ', tunable=TunableTuple( target_game_object=TunableReference( description= '\n The definition of the object that will be created/destroyed/altered\n by the game.\n ', manager=services.definition_manager()), parent_slot=TunableVariant( description= '\n The slot on the parent object where the target_game_object object should go. This\n may be either the exact name of a bone on the parent object or a\n slot type, in which case the first empty slot of the specified type\n in which the child object fits will be used.\n ', by_name=Tunable( description= '\n The exact name of a slot on the parent object in which the target\n game object should go. \n ', tunable_type=str, default='_ctnm_'), by_reference=TunableReference( description= '\n A particular slot type in which the target game object should go. The\n first empty slot of this type found on the parent will be used.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE))))) }
class Puddle(objects.game_object.GameObject): __qualname__ = 'Puddle' WEED_DEFINITIONS = TunableDefinitionList( description= '\n Possible weed objects which can be spawned by evaporation.') PLANT_DEFINITIONS = TunableDefinitionList( description= '\n Possible plant objects which can be spawned by evaporation.' ) INSTANCE_TUNABLES = { 'indoor_evaporation_time': TunableInterval( description= '\n Number of SimMinutes this puddle should take to evaporate when \n created indoors.\n ', tunable_type=TunableSimMinute, default_lower=200, default_upper=300, minimum=1, tuning_group=GroupNames.PUDDLES), 'outdoor_evaporation_time': TunableInterval( description= '\n Number of SimMinutes this puddle should take to evaporate when \n created outdoors.\n ', tunable_type=TunableSimMinute, default_lower=30, default_upper=60, minimum=1, tuning_group=GroupNames.PUDDLES), 'evaporation_outcome': TunableTuple( nothing=TunableRange(int, 5, minimum=1, description='Relative chance of nothing.'), weeds=TunableRange(int, 2, minimum=0, description='Relative chance of weeds.'), plant=TunableRange(int, 1, minimum=0, description='Relative chance of plant.'), tuning_group=GroupNames.PUDDLES), 'intial_stat_value': TunableTuple( description= '\n This is the starting value for the stat specified. This controls \n how long it takes to mop this puddle.\n ', stat=Statistic.TunableReference( description= '\n The stat used for mopping puddles.\n ' ), value=Tunable( description= '\n The initial value this puddle should have for the mopping stat.\n The lower the value (-100,100), the longer it takes to mop up.\n ', tunable_type=int, default=-20), tuning_group=GroupNames.PUDDLES) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._puddle_liquid = None self._puddle_size = None self._evaporate_alarm_handle = None self.statistic_tracker.set_value(self.intial_stat_value.stat, self.intial_stat_value.value) @property def size_count(self): if self._puddle_size == PuddleSize.SmallPuddle: return 1 if self._puddle_size == PuddleSize.MediumPuddle: return 2 if self._puddle_size == PuddleSize.LargePuddle: return 3 def place_puddle(self, target, max_distance, ids_to_ignore=DEFAULT): destroy_puddle = True try: if ids_to_ignore is DEFAULT: ids_to_ignore = (self.id, ) else: ids_to_ignore.append(self.id) flags = placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_POSITIONS flags = flags | placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_INTENDED_POSITIONS flags = flags | placement.FGLSearchFlag.STAY_IN_SAME_CONNECTIVITY_GROUP if target.is_on_active_lot(): flags = flags | placement.FGLSearchFlag.SHOULD_TEST_BUILDBUY else: flags = flags | placement.FGLSearchFlag.SHOULD_TEST_ROUTING flags = flags | placement.FGLSearchFlag.USE_SIM_FOOTPRINT flags = flags | placement.FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS flags = flags | placement.FGLSearchFlag.DONE_ON_MAX_RESULTS radius_target = target while radius_target.parent is not None: radius_target = radius_target.parent if radius_target.is_part: radius_target = radius_target.part_owner fgl_context = placement.FindGoodLocationContext( starting_position=target.position + target.forward * radius_target.object_radius, starting_orientation=sims4.random.random_orientation(), starting_routing_surface=target.routing_surface, object_id=self.id, ignored_object_ids=ids_to_ignore, max_distance=max_distance, search_flags=flags) (position, orientation) = placement.find_good_location(fgl_context) if position is not None: destroy_puddle = False self.location = sims4.math.Location( sims4.math.Transform(position, orientation), target.routing_surface) self.fade_in() self.start_evaporation() return True return False finally: if destroy_puddle: self.destroy(source=self, cause='Failed to place puddle.') def try_grow_puddle(self): if self._puddle_size == PuddleSize.LargePuddle: return if self._puddle_size == PuddleSize.MediumPuddle: puddle = create_puddle(PuddleSize.LargePuddle, puddle_liquid=self._puddle_liquid) else: puddle = create_puddle(PuddleSize.MediumPuddle, puddle_liquid=self._puddle_liquid) if puddle.place_puddle(self, 1, ids_to_ignore=[self.id]): if self._evaporate_alarm_handle is not None: alarms.cancel_alarm(self._evaporate_alarm_handle) self.fade_and_destroy() return puddle def start_evaporation(self): if self._evaporate_alarm_handle is not None: alarms.cancel_alarm(self._evaporate_alarm_handle) if self.is_outside: time = self.outdoor_evaporation_time.random_float() else: time = self.indoor_evaporation_time.random_float() self._evaporate_alarm_handle = alarms.add_alarm( self, date_and_time.create_time_span(minutes=time), self.evaporate) def evaporate(self, handle): if self.in_use: self.start_evaporation() return if self.is_on_natural_ground(): defs_to_make = sims4.random.weighted_random_item([ (self.evaporation_outcome.nothing, None), (self.evaporation_outcome.weeds, self.WEED_DEFINITIONS), (self.evaporation_outcome.plant, self.PLANT_DEFINITIONS) ]) if defs_to_make: def_to_make = random.choice(defs_to_make) obj_location = sims4.math.Location( sims4.math.Transform(self.position, sims4.random.random_orientation()), self.routing_surface) (result, _) = build_buy.test_location_for_object( None, def_to_make.id, obj_location, [self]) if result: obj = objects.system.create_object(def_to_make) obj.opacity = 0 obj.location = self.location obj.fade_in() self._evaporate_alarm_handle = None self.fade_and_destroy() def load_object(self, object_data): super().load_object(object_data) if PuddleChoices.reverse_lookup is None: PuddleChoices.reverse_lookup = {} for (liquid, sizelists) in PuddleChoices.PUDDLE_DEFINITIONS.items(): for (size, definitions) in sizelists.items(): for definition in definitions: PuddleChoices.reverse_lookup[definition] = (liquid, size) if self.definition in PuddleChoices.reverse_lookup: (liquid, size) = PuddleChoices.reverse_lookup[self.definition] self._puddle_liquid = liquid self._puddle_size = size return logger.error('Unknown size/liquid for puddle: {} on load.', self, owner='nbaker') self._puddle_size = PuddleSize.MediumPuddle self._puddle_liquid = PuddleLiquid.WATER
class CardBattleBehavior(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'collectable_type': TunableEnumEntry( description= '\n Id for the card battle collection where the collectible items\n will be read when a new card needs to be created.\n ', tunable_type=CollectionIdentifier, default=CollectionIdentifier.Unindentified, invalid_enums=(CollectionIdentifier.Unindentified, )), 'card_slot_type': TunableReference( description= '\n Slot type where player card should appear.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE)), 'practice_card': TunableReference( description= '\n Object reference to use as the default definition as the opponent\n card. This is to have the same dummy as the opponent when game is\n only played by one player.\n ', manager=services.definition_manager()), 'challenger_buff': TunableBuffReference( description= '\n The buff to apply to the Sim that started the game. This is used\n to be able to guarantee we maintain the challenger Sim consistent\n since the setup mixers and turns can be run by other Sims\n depending on route time and other aspects.\n ' ), 'card_information': TunableTuple( description= '\n Challenger and defender information that will be used to identify\n specific behavior of the cards depending on their placement.\n ', challenge_state_value=ObjectStateValue.TunableReference( description= '\n The state value cards will have when they are selected for \n a game challenge.\n ' ), default_state_value=ObjectStateValue.TunableReference( description= '\n Default state value of cards after a challenge is done.\n ' ), level_state=ObjectState.TunableReference( description= '\n Level states defining the state values that the card has\n representing its experience level.\n ' ), challenger_prop_override=Tunable( description= '\n Prop override name for the card placed on the challenger slot.\n Name for prop should match prop name on swing. \n ', tunable_type=str, default=''), defender_prop_override=Tunable( description= '\n Prop override name for the card placed on the defender slot.\n Name for prop should match prop name on swing.\n ', tunable_type=str, default='')), 'card_scoring': TunableTuple( description= '\n Scoring tunables to apply to a card when the game ends.\n ', level_statistic=Statistic.TunableReference( description= '\n This statistic is used as the level statistic value to be\n increased when the card has won a game.\n ' ), game_won_statistic_increase=TunableRange( description= '\n Statistic value to increase if the game is won.\n Final score increase is affected by the state to stat\n multiplier.\n ', tunable_type=int, default=1, minimum=0), game_lost_statistic_increase=TunableRange( description= '\n Statistic value to increase if the game is lost.\n Final score increase is affected by the state to stat\n multiplier.\n ', tunable_type=int, default=1, minimum=0), state_to_stat_multiplier=TunableMapping( description= "\n Mapping of card state value to stat multiplier when a game is \n finished.\n This value will be multiplied by the \n game_won_statistic_increase or game_lost_statistic_increase\n depending if it's a win or a loss.\n e.g. If card has LEVEL_TWO state value, experience per win is \n game_won_statistic_increase * multiplier corresponding to the\n LEVEL_TWO state value.\n ", key_type=ObjectStateValue.TunableReference( description= '\n State value the card should have to apply this multiplier\n to the statistic increase.\n ' ), value_type=TunableRange( description= '\n Multiplier that affects the game won statistic increase \n on the card.\n ', tunable_type=float, default=1, minimum=0))), 'placement_state_buff': TunableList( description= '\n List of states and buffs to be applied to the Sim when a card\n with active state value.\n ', tunable=TunableTuple( description= '\n Tuple of state and buff that will be added to the Sim when\n a card with that specific state value is played.\n ', state_value=ObjectStateValue.TunableReference( description= '\n Object state value card needs to have to add the buff\n into the Sim.\n ', pack_safe=True), buff=TunableBuffReference( description= '\n The buff to apply to the Sim when a card with this state\n is played.\n ', pack_safe=True))), 'card_tag': TunableEnumWithFilter( description= '\n Tag to look for when iterating through objects to know if they\n are of the card type.\n ', tunable_type=tag.Tag, filter_prefixes=['object', 'func'], default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._players_cards = {} self.challenger_definition = None self.defender_definition = None self._arena_obj = None def on_player_added(self, sim, target): self._arena_obj = target.part_owner candidate_cards = [] player_card = None sim_inventory = sim.inventory_component from_inventory = True player_slot = self._get_slot_for_sim_position(target, sim.position) slotted_objects = player_slot.children if slotted_objects: player_card = player_slot.children[0] if sim.is_npc: from_inventory = False else: for obj in sim_inventory: if obj.definition.has_build_buy_tag(self.card_tag): if obj.state_value_active( self.card_information.challenge_state_value): player_card = obj player_card.set_state( self.card_information.default_state_value.state, self.card_information.default_state_value) break candidate_cards.append(obj) if player_card is None: if candidate_cards: player_card = random.choice(candidate_cards) else: from_inventory = False card_options = ObjectCollectionData.get_collection_data( self.collectable_type).object_list if not card_options: logger.error('Collection {} is an invalid id', self.collectable_type) return card_definition = random.choice( card_options).collectable_item player_card = create_object(card_definition) card_level_state_value = random.choice( self.card_information.level_state.values) player_card.set_state(card_level_state_value.state, card_level_state_value) player_card.persistence_group = PersistenceGroups.NONE if player_card is None: logger.error( 'Failed to create card for player {} for card candidates {}', sim, candidate_cards) card_definition = player_card.get_game_animation_definition() if card_definition is None: logger.error( 'Card {} has no game animation definition tuned and will not be displayed on the card battle object', player_card) return if self.challenger_definition is None: self.challenger_definition = card_definition sim.add_buff_from_op(buff_type=self.challenger_buff.buff_type, buff_reason=self.challenger_buff.buff_reason) else: self.defender_definition = card_definition self._create_card_on_slot(player_card, player_slot) self._apply_card_placement_bonus(sim, player_card) reservation_handler = player_card.get_reservation_handler(sim) reservation_handler.begin_reservation() self._players_cards[sim] = (player_card, from_inventory, reservation_handler) def on_setup_game(self, game_object): pass def on_game_ended(self, winning_team, game_object): for sim in list(self._players_cards): if winning_team is not None: if sim in winning_team.players: self._update_card_scoring( sim, self.card_scoring.game_won_statistic_increase) else: self._update_card_scoring( sim, self.card_scoring.game_lost_statistic_increase) self.on_player_removed(sim, from_game_ended=True) self.challenger_definition = None self.defender_definition = None self._arena_obj = None def _update_card_scoring(self, sim, win_loss_score): (card, from_inventory, _) = self._players_cards[sim] if card is None: logger.error( 'Game ended but Sim {} was removed earlier, this will cause cards to not be updated', sim) return if not from_inventory: return level_state_value = card.get_state(self.card_information.level_state) if level_state_value is None: logger.error( "Card {} doesn't support the state {} used for card scoring", card, self.card_information.level_state) return score_multiplier = self.card_scoring.state_to_stat_multiplier.get( level_state_value) if score_multiplier is None: logger.error( 'Card scoring tuning error, state value {} is not tuned inside the multiplier range of the game', level_state_value) return level_statistic = card.get_stat_instance( self.card_scoring.level_statistic, add=True) if level_statistic is not None: level_statistic.tracker.add_value( self.card_scoring.level_statistic, win_loss_score * score_multiplier) def _apply_card_placement_bonus(self, sim, card): for placement_modifier in self.placement_state_buff: if card.state_value_active(placement_modifier.state_value): sim.add_buff_from_op( buff_type=placement_modifier.buff.buff_type, buff_reason=placement_modifier.buff.buff_reason) def on_player_removed(self, sim, from_game_ended=False): if sim not in self._players_cards: return if not from_game_ended: self._update_card_scoring( sim, self.card_scoring.game_lost_statistic_increase) (card, from_inventory, reservation_handler) = self._players_cards[sim] reservation_handler.end_reservation() if from_inventory: sim.inventory_component.player_try_add_object(card) else: card.set_parent(None) card.destroy(source=self, cause='GameComponent: Placeholder game card removed.') del self._players_cards[sim] if sim.has_buff(self.challenger_buff.buff_type): sim.remove_buff_by_type(self.challenger_buff.buff_type) def _create_card_on_slot(self, card, slot): if slot is not None and slot.empty: slot.add_child(card) def _get_slot_for_sim_position(self, target, sim_position): max_magnitude = None closest_slot = None for runtime_slot in target.part_owner.get_runtime_slots_gen( slot_types={self.card_slot_type}): difference_vector = runtime_slot.position - sim_position difference_magnitude = difference_vector.magnitude() if not max_magnitude is None: if difference_magnitude < max_magnitude: closest_slot = runtime_slot max_magnitude = difference_magnitude closest_slot = runtime_slot max_magnitude = difference_magnitude return closest_slot def additional_anim_overrides_gen(self): prop_overrides = {} if self.challenger_definition is not None: self._set_prop_override( prop_overrides, self.card_information.challenger_prop_override, self.challenger_definition) if self.defender_definition is None: self._set_prop_override( prop_overrides, self.card_information.defender_prop_override, self.practice_card) if self.defender_definition is not None: self._set_prop_override( prop_overrides, self.card_information.defender_prop_override, self.defender_definition) if self.challenger_definition is None: self._set_prop_override( prop_overrides, self.card_information.challenger_prop_override, self.practice_card) yield AnimationOverrides(props=prop_overrides) def _set_prop_override(self, prop_overrides, override_name, card_definition): prop_overrides[override_name] = sims4.collections.FrozenAttributeDict({ 'states_to_override': (), 'from_actor': None, 'definition': card_definition, 'sharing': None, 'set_as_actor': None })
class GameRules(HasTunableReference, metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.GAME_RULESET)): ENDING_CONDITION_SCORE = 0 ENDING_CONDITION_ROUND = 1 INSTANCE_TUNABLES = { 'game_name': TunableLocalizedStringFactory( description='\n Name of the game.\n ', default=1860708663), 'team_strategy': TunableVariant( description= '\n Define how Sims are distributed across teams.\n ', auto_balanced=GameTeamAutoBalanced.TunableFactory(), part_driven=GameTeamPartDriven.TunableFactory(), default='auto_balanced'), 'teams_per_game': TunableInterval( description= '\n An interval specifying the number of teams allowed per game.\n \n Joining Sims are put on a new team if the maximum number of teams\n has not yet been met, otherwise they are put into the team with the\n fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_game': TunableInterval( description= '\n An interval specifying the number of players allowed per game.\n \n If the maximum number of players has not been met, Sims can\n continue to join a game. Joining Sims are put on a new team if the\n maximum number of teams as specified in the "teams_per_game"\n tunable has not yet been met, otherwise they are put into the team\n with the fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_turn': TunableRange( description= '\n An integer specifying number of players from the active team who\n take their turn at one time.\n ', tunable_type=int, default=1, minimum=1), 'initial_state': ObjectStateValue.TunableReference( description= "\n The game's starting object state.\n ", allow_none=True), 'score_info': TunableTuple( description= "\n Tunables that affect the game's score.\n ", ending_condition=TunableVariant( description= '\n The condition under which the game ends.\n ', score_based=TunableTuple( description= '\n A game that ends when one of the teams wins by reaching a \n certain score first\n ', locked_args={'end_condition': ENDING_CONDITION_SCORE}, winning_score=Tunable( description= '\n Score required to win.\n ', tunable_type=int, default=100)), round_based=TunableTuple( description= '\n A game that ends after a certain number of rounds. The Team\n with the highest score at that point wins.\n ', locked_args={'end_condition': ENDING_CONDITION_ROUND}, rounds=Tunable( description= '\n Length of game (in rounds).\n ', tunable_type=int, default=3)), default='score_based'), score_increase=TunableInterval( description= '\n An interval specifying the minimum and maximum score increases\n possible in one turn. A random value in this interval will be\n generated each time score loot is given.\n ', tunable_type=int, default_lower=35, default_upper=50, minimum=0), allow_scoring_for_non_active_teams=Tunable( description= '\n If checked, any Sim may score, even if their team is not\n considered active.\n ', tunable_type=bool, default=False), skill_level_bonus=Tunable( description= "\n A bonus number of points based on the Sim's skill level in the\n relevant_skill tunable that will be added to score_increase.\n \n ex: If this value is 2 and the Sim receiving score has a\n relevant skill level of 4, they will receive 8 (2 * 4) extra\n points.\n ", tunable_type=float, default=2), relevant_skill=Skill.TunableReference( description= "\n The skill relevant to this game. Each Sim's proficiency in\n this skill will effect the score increase they get.\n ", allow_none=True), use_effective_skill_level=Tunable( description= '\n If checked, we will use the effective skill level rather than\n the actual skill level of the relevant_skill tunable.\n ', tunable_type=bool, default=True), progress_stat=Statistic.TunableReference( description= '\n The statistic that advances the progress state of this game.\n ', allow_none=True), persist_high_score=Tunable( description= '\n If checked, the high score and the team Sim ids will be\n saved onto the game component.\n ', tunable_type=bool, default=False)), 'clear_score_on_player_join': Tunable( description= '\n Tunable that, when checked, will clear the game score when a player joins.\n \n This essentially resets the game.\n ', tunable_type=bool, default=False), 'alternate_target_object': OptionalTunable( description= '\n Tunable that, when enabled, means the game should create an alternate object\n in the specified slot on setup that will be modified as the game goes on\n and destroyed when the game ends.\n ', tunable=TunableTuple( target_game_object=TunableReference( description= '\n The definition of the object that will be created/destroyed/altered\n by the game.\n ', manager=services.definition_manager()), parent_slot=TunableVariant( description= '\n The slot on the parent object where the target_game_object object should go. This\n may be either the exact name of a bone on the parent object or a\n slot type, in which case the first empty slot of the specified type\n in which the child object fits will be used.\n ', by_name=Tunable( description= '\n The exact name of a slot on the parent object in which the target\n game object should go. \n ', tunable_type=str, default='_ctnm_'), by_reference=TunableReference( description= '\n A particular slot type in which the target game object should go. The\n first empty slot of this type found on the parent will be used.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE))), destroy_at_end=Tunable( description= '\n If True, the alternate target object will get destroyed at the end of the game.\n ', tunable_type=bool, default=True))), 'game_over_notification': OptionalTunable( description= "\n If enabled, when any Sim involved in the game is a player-controlled\n Sim, display a notification when the game is over.\n \n NOTE: As of now, this only triggers when there are *exactly* two\n teams. To support more teams, we'll need to extend the possible\n string permutation.\n ", tunable=TunableTuple( one_v_one=TunableUiDialogNotificationSnippet( description= "\n The notification to show when the game is 1v1.\n \n * Token 0 is the object the game is being played on\n * Token 1 is the winner\n * Token 2 is the loser\n * Token 3 is the winner's score\n * Token 4 is the loser's score\n " ), one_v_many_winner =TunableUiDialogNotificationSnippet( description= "\n The notification to show when the game is 1 v many, and the\n single Sim is the winner.\n \n * Token 0 is the object the game is being played on\n * Token 1 is the winner\n * Token 2 is a list of losers (Alice, Bob, and Carol)\n * Token 3 is the winner's score\n * Token 4 is the loser's score\n " ), one_v_many_loser=TunableUiDialogNotificationSnippet( description= "\n The notification to show when the game is 1 v many, and the\n single Sim is the loser.\n \n * Token 0 is the object the game is being played on\n * Token 1 is a list of winners (Alice, Bob, and Carol)\n * Token 2 is the loser\n * Token 3 is the winner's score\n * Token 4 is the loser's score\n " ), many_v_many=TunableUiDialogNotificationSnippet( description= "\n The notification to show when the game is many v many.\n \n * Token 0 is the object the game is being played on\n * Token 1 is a list of winners (Alice and Bob)\n * Token 2 is a list of losers (Carol, Dan, and Erin)\n * Token 3 is the winner's score\n * Token 4 is the loser's score\n " ))), 'game_over_winner_only_notification': OptionalTunable( description= '\n If enabled, when any Sim involved in the game is a player-controlled\n Sim, display a notification when the game is over.\n \n NOTE: This will show only the winners of the game with the highest \n score. The winners can be more than one team if they have same \n score.\n ', tunable=TunableTuple( play_alone=TunableUiDialogNotificationSnippet( description= "\n The notification to show when Sim play alone.\n \n * Token 0 is the object the game is being played on\n * Token 1 is the Sim's name\n * Token 2 is the Sim's score\n " ), winner=TunableUiDialogNotificationSnippet( description= "\n The notification to show when the game has 1 team winner.\n \n * Token 0 is the object the game is being played on\n * Token 1 is the winner\n * Token 2 is the winner's score\n " ))), 'additional_game_behavior': OptionalTunable( description= '\n If enabled additional behavior will be run for this type of game\n on multiple phases like creating destroying additional objects on \n setup of end phases.\n ', tunable=TunableVariant( description= "\n Variant of type of games that will add very specific behavior\n to the game component.\n e.g. Card battle behavior will create cards and destroy them\n depending on each actor's inventory.\n ", card_battle=CardBattleBehavior.TunableFactory(), create_object=CreateObjectBehavior.TunableFactory(), default='card_battle')) } def __init__(self, game_component, *args, **kwargs): super().__init__(*args, **kwargs) self._game_component = game_component self.additional_game_behavior = self.additional_game_behavior( ) if self.additional_game_behavior is not None else None def add_player(self, sim): self.team_strategy.add_player(self._game_component, sim) @classmethod def can_be_on_same_team(cls, target_a, target_b): return cls.team_strategy.can_be_on_same_team(target_a, target_b) @classmethod def team_determines_part(cls): return cls.team_strategy.team_determines_part() @classmethod def can_be_on_opposing_team(cls, target_a, target_b): return cls.team_strategy.can_be_on_opposing_team(target_a, target_b) def remove_player(self, sim): self.team_strategy.remove_player(self._game_component, sim)
class ObjectRelationshipComponent(Component, HasTunableFactory, component_name=types.OBJECT_RELATIONSHIP_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent): __qualname__ = 'ObjectRelationshipComponent' FACTORY_TUNABLES = {'number_of_allowed_relationships': OptionalTunable(description='\n Number of Sims who can have a relationship with this object at one\n time. If not specified, an infinite number of Sims can have a \n relationship with the object.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), 'relationship_stat': Statistic.TunableReference(description="\n The statistic which will be created for each of this object's\n relationships.\n "), 'relationship_track_visual': OptionalTunable(RelationshipTrack.TunableReference(description='\n The relationship that this track will visually try and imitate in\n regards to static track tack data. If this is None then this\n relationship will not be sent down to the client.\n ')), 'relationship_based_state_change_tuning': OptionalTunable(TunableTuple(description='\n A list of value ranges and associated states. If the active Sim\n has a relationship with this object that falls within one of the\n value ranges specified here, the object will change state to match\n the specified state.\n \n These state changes exist on a per Sim basis, so this tuning will\n effectively make the same object appear different depending on\n which Sim is currently active.\n ', state_changes=TunableList(tunable=TunableTuple(value_threshold=TunableThreshold(description="\n The range that the active Sim's relationship with this\n object must fall within in order for this state change to\n take place.\n "), state=TunableStateValueReference(description="\n The state this object will change to if it's relationship\n with the active Sim falls within the specified range.\n "))), default_state=TunableStateValueReference(description='\n The state this object will change to if there is no other tuned\n relationship based state change for the currently active Sim.\n ')))} def __init__(self, owner, number_of_allowed_relationships, relationship_stat, relationship_track_visual, relationship_based_state_change_tuning): super().__init__(owner) self._number_of_allowed_relationships = number_of_allowed_relationships self._relationship_stat = relationship_stat self._relationship_track_visual = relationship_track_visual self._relationship_based_state_change_tuning = relationship_based_state_change_tuning self._state_changes = None self._default_state = None self._relationships = {} self._relationship_changed_callbacks = defaultdict(CallableList) def _on_active_sim_change(self, _, new_sim): if new_sim is None: return relationship = self._get_relationship(new_sim.id) self._update_state(relationship) def _update_state(self, relationship): if self._default_state is None: return if relationship is None: new_state = self._default_state elif self._state_changes is None: new_state = self._default_state else: for state_change in self._state_changes: while state_change.value_threshold.compare(relationship.get_value()): new_state = state_change.state break new_state = self._default_state self.owner.set_state(new_state.state, new_state) @property def _can_add_new_relationship(self): if self._number_of_allowed_relationships is not None and len(self._relationships) >= self._number_of_allowed_relationships: return False return True def on_add(self): if self._relationship_based_state_change_tuning is None: return self._state_changes = self._relationship_based_state_change_tuning.state_changes self._default_state = self._relationship_based_state_change_tuning.default_state services.current_zone().register_callback(zone_types.ZoneState.CLIENT_CONNECTED, self._register_active_sim_change) services.current_zone().register_callback(zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED, self._publish_relationship_data) def on_remove(self): client = services.client_manager().get_first_client() if client is not None: client.unregister_active_sim_changed(self._on_active_sim_change) def _register_active_sim_change(self): client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(self._on_active_sim_change) def _publish_relationship_data(self): if not self._relationship_track_visual: return for sim_id in self._relationships.keys(): self._send_relationship_data(sim_id) def add_relationship_changed_callback_for_sim_id(self, sim_id, callback): self._relationship_changed_callbacks[sim_id].append(callback) def remove_relationship_changed_callback_for_sim_id(self, sim_id, callback): if sim_id in self._relationship_changed_callbacks and callback in self._relationship_changed_callbacks[sim_id]: self._relationship_changed_callbacks[sim_id].remove(callback) def _trigger_relationship_changed_callbacks_for_sim_id(self, sim_id): callbacks = self._relationship_changed_callbacks[sim_id] if callbacks is not None: callbacks() def add_relationship(self, sim_id): if sim_id in self._relationships: return False if not self._can_add_new_relationship: return False stat = self._relationship_stat(None) self._relationships[sim_id] = stat stat.on_add() self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) return True def remove_relationship(self, sim_id): if sim_id not in self._relationships: return del self._relationships[sim_id] self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) def modify_relationship(self, sim_id, value, add=True): if not add: return if not (sim_id not in self._relationships and self.add_relationship(sim_id)): return self._relationships[sim_id].add_value(value) self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) client = services.client_manager().get_first_client() if client is not None and client.active_sim is not None and client.active_sim.sim_id == sim_id: self._update_state(self._relationships[sim_id]) def _get_relationship(self, sim_id): return self._relationships.get(sim_id) def has_relationship(self, sim_id): return sim_id in self._relationships def get_relationship_value(self, sim_id): relationship = self._get_relationship(sim_id) if relationship is not None: return relationship.get_value() return self._relationship_stat.initial_value def get_relationship_initial_value(self): return self._relationship_stat.initial_value def _send_relationship_data(self, sim_id): if self._relationship_track_visual is None: return relationship_to_send = self._get_relationship(sim_id) if not relationship_to_send: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipUpdate() msg.actor_sim_id = sim_id (msg.target_id.object_id, msg.target_id.manager_id) = self.owner.icon_info msg.target_instance_id = self.owner.id with ProtocolBufferRollback(msg.tracks) as relationship_track_update: relationship_value = relationship_to_send.get_value() relationship_track_update.track_score = relationship_value relationship_track_update.track_bit_id = self._relationship_track_visual.get_bit_at_relationship_value(relationship_value).guid64 relationship_track_update.track_id = self._relationship_track_visual.guid64 relationship_track_update.track_popup_priority = self._relationship_track_visual.display_popup_priority send_relationship_op(sim_info, msg) def save(self, persistence_master_message): if not self._relationships: return persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent relationship_component_data = persistable_data.Extensions[protocols.PersistableObjectRelationshipComponent.persistable_data] for (key, value) in self._relationships.items(): with ProtocolBufferRollback(relationship_component_data.relationships) as relationship_data: relationship_data.sim_id = key relationship_data.value = value.get_value() persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): relationship_component_data = persistable_data.Extensions[protocols.PersistableObjectRelationshipComponent.persistable_data] for relationship in relationship_component_data.relationships: self.modify_relationship(relationship.sim_id, relationship.value)
class ObjectRelationshipComponent( Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_RELATIONSHIP_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData. ObjectRelationshipComponent): FACTORY_TUNABLES = { 'number_of_allowed_relationships': OptionalTunable( description= '\n Number of Sims who can have a relationship with this object at one\n time. If not specified, an infinite number of Sims can have a \n relationship with the object.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), 'icon_override': OptionalTunable( description= "\n If enabled, this will override the object's thumbnail generated \n default icon on Relationship panel.\n ", tunable=TunableResourceKey( description= '\n The icon to be displayed in the Relationship panel.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All)), 'relationship_stat': Statistic.TunableReference( description= "\n The statistic which will be created for each of this object's\n relationships.\n " ), 'relationship_track_visual': OptionalTunable( description= '\n If enabled, the relationship track to send the client and where\n it should be displayed. If this is None then this relationship will \n not be sent down to the client.\n ', tunable=TunableTuple( relationship_track=RelationshipTrack.TunableReference( description= '\n The relationship that this track will visually try and imitate in\n regards to static track tack data.\n ' ), visible_in_relationship_panel=Tunable( description= "\n By default the relationship is visible in the relationship \n panel and the object's tooltip. If this is set to false, \n hide the relationship from the relationship panel. \n ", tunable_type=bool, default=True))), 'relationship_based_state_change_tuning': OptionalTunable( TunableTuple( description= '\n A list of value ranges and associated states. If the active Sim\n has a relationship with this object that falls within one of the\n value ranges specified here, the object will change state to match\n the specified state.\n \n These state changes exist on a per Sim basis, so this tuning will\n effectively make the same object appear different depending on\n which Sim is currently active.\n ', state_changes=TunableList( tunable=TunableTuple( value_threshold=TunableThreshold( description= "\n The range that the active Sim's relationship with this\n object must fall within in order for this state change to\n take place.\n " ), state=TunableStateValueReference( description= "\n The state this object will change to if it's relationship\n with the active Sim falls within the specified range.\n " ))), default_state=TunableStateValueReference( description= '\n The state this object will change to if there is no other tuned\n relationship based state change for the currently active Sim.\n ' ))) } def __init__(self, owner, **kwargs): super().__init__(owner, **kwargs) self._state_changes = None self._default_state = None self._object_social_mixin = None self._relationships = {} self._relationship_changed_callbacks = defaultdict(CallableList) self._definition_changed_in_buildbuy = False @staticmethod def setup_relationship(sim, target_object): if target_object.objectrelationship_component is None: logger.error( "Failed to add object relationship because {} doesn't have objectrelationship_component tuned", target_object) return if target_object.objectrelationship_component.has_relationship(sim.id): logger.error('Relationship already exists between {} and {}.', sim, target_object) return if not target_object.objectrelationship_component.add_relationship( sim.id): logger.error( 'Failed to add new object relationship between {} and {}.', sim, target_object) @property def relationships(self): return self._relationships def get_number_of_allowed_relationships(self): return self.number_of_allowed_relationships def _on_active_sim_change(self, _, new_sim): if new_sim is None: return relationship = self._get_relationship(new_sim.id) self._update_state(relationship) def _update_state(self, relationship): if self._default_state is None: return if relationship is None: new_state = self._default_state elif self._state_changes is None: new_state = self._default_state else: for state_change in self._state_changes: if state_change.value_threshold.compare( relationship.get_value()): new_state = state_change.state break else: new_state = self._default_state self.owner.set_state(new_state.state, new_state) @property def _can_add_new_relationship(self): if self.number_of_allowed_relationships is not None and len( self._relationships) >= self.number_of_allowed_relationships: return False return True def on_add(self): services.current_zone().register_callback( zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED, self._publish_relationship_data) if self.relationship_based_state_change_tuning is None: return self._state_changes = self.relationship_based_state_change_tuning.state_changes self._default_state = self.relationship_based_state_change_tuning.default_state services.current_zone().register_callback( zone_types.ZoneState.CLIENT_CONNECTED, self._register_active_sim_change) def on_remove(self): client = services.client_manager().get_first_client() if client is not None: client.unregister_active_sim_changed(self._on_active_sim_change) self.owner.remove_name_changed_callback(self._on_name_changed) self.destroy_all_relationship() def apply_definition(self, definition, obj_state=0): if not services.current_zone().is_in_build_buy: return self._definition_changed_in_buildbuy |= self.owner.definition != definition def on_buildbuy_exit(self): if not self._definition_changed_in_buildbuy: return self._publish_relationship_data() self._definition_changed_in_buildbuy = False def _register_active_sim_change(self): client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(self._on_active_sim_change) def _publish_relationship_data(self): if self.relationship_track_visual is None: return for sim_id in self._relationships.keys(): self._send_relationship_data(sim_id) def _update_object_relationship_name(self): ownable_component = self.owner.get_component(types.OWNABLE_COMPONENT) if ownable_component is not None: sim_owner_id = ownable_component.get_sim_owner_id() obj_def_id = self.owner.definition.id relationship_service = services.relationship_service() obj_tag_set = relationship_service.get_mapped_tag_set_of_id( obj_def_id) if obj_tag_set is not None: obj_relationship = relationship_service.get_object_relationship( sim_owner_id, obj_tag_set) if obj_relationship is not None and self.owner.has_custom_name( ): obj_relationship.set_object_rel_name( self.owner.custom_name) def _on_name_changed(self, *_, **__): self._publish_relationship_data() self._update_object_relationship_name() def add_relationship_changed_callback_for_sim_id(self, sim_id, callback): self._relationship_changed_callbacks[sim_id].append(callback) def remove_relationship_changed_callback_for_sim_id( self, sim_id, callback): if sim_id in self._relationship_changed_callbacks and callback in self._relationship_changed_callbacks[ sim_id]: self._relationship_changed_callbacks[sim_id].remove(callback) def _trigger_relationship_changed_callbacks_for_sim_id(self, sim_id): callbacks = self._relationship_changed_callbacks[sim_id] if callbacks is not None: callbacks() def add_relationship(self, sim_id): if sim_id in self._relationships: return False if not self._can_add_new_relationship: return False self.owner.on_hovertip_requested() stat = self.relationship_stat(None) self._relationships[sim_id] = stat stat.on_add() self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) self.owner.add_name_changed_callback(self._on_name_changed) return True def remove_relationship(self, sim_id): if sim_id not in self._relationships: return del self._relationships[sim_id] self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) self._send_relationship_destroy(sim_id) def destroy_all_relationship(self): sim_ids = list(self._relationships.keys()) for sim_id in sim_ids: self.remove_relationship(sim_id) def modify_relationship(self, sim_id, value, add=True, from_load=False): if sim_id not in self._relationships: if not add: return if not self.add_relationship(sim_id): return if from_load: self._relationships[sim_id].set_value(value) else: self._relationships[sim_id].add_value(value) self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) client = services.client_manager().get_first_client() if client is not None and client.active_sim is not None and client.active_sim.sim_id == sim_id: self._update_state(self._relationships[sim_id]) def on_social_start(self, sim): if self.has_relationship(sim.id) or self.add_relationship(sim.id): self._object_social_mixin = ObjectRelationshipSocialMixin( sim, self.owner.id, self._get_relationship(sim.id)) self._object_social_mixin.send_social_start_message() def on_social_end(self): if self._object_social_mixin is not None: self._object_social_mixin.send_social_end_message() self._object_social_mixin = None def _get_relationship(self, sim_id): return self._relationships.get(sim_id) def has_relationship(self, sim_id): return sim_id in self._relationships def get_relationship_value(self, sim_id): relationship = self._get_relationship(sim_id) if relationship is not None: return relationship.get_value() return self.relationship_stat.initial_value def get_relationship_initial_value(self): return self.relationship_stat.initial_value def get_relationship_max_value(self): return self.relationship_stat.max_value def get_relationship_min_value(self): return self.relationship_stat.min_value def _send_relationship_data(self, sim_id): if self.relationship_track_visual is None: return relationship_to_send = self._get_relationship(sim_id) if not relationship_to_send: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipUpdate() msg.actor_sim_id = sim_id (msg.target_id.object_id, msg.target_id.manager_id) = self.owner.icon_info msg.target_instance_id = self.owner.id if self.icon_override is not None: build_icon_info_msg(IconInfoData(icon_resource=self.icon_override), None, msg.target_icon_override) with ProtocolBufferRollback(msg.tracks) as relationship_track_update: relationship_value = relationship_to_send.get_value() relationship_track_update.track_score = relationship_value relationship_track_update.track_bit_id = self.relationship_track_visual.relationship_track.get_bit_at_relationship_value( relationship_value).guid64 relationship_track_update.track_id = self.relationship_track_visual.relationship_track.guid64 relationship_track_update.track_popup_priority = self.relationship_track_visual.relationship_track.display_popup_priority relationship_track_update.visible_in_relationship_panel = self.relationship_track_visual.visible_in_relationship_panel send_relationship_op(sim_info, msg) if self._object_social_mixin is not None: self._object_social_mixin.send_social_update_message() def _send_relationship_destroy(self, sim_id): if self.relationship_track_visual is None or self.relationship_track_visual.relationship_track is None: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipDelete() msg.actor_sim_id = sim_id msg.target_id = self.owner.id op = GenericProtocolBufferOp( DistributorOps_pb2.Operation.SIM_RELATIONSHIP_DELETE, msg) distributor = Distributor.instance() distributor.add_op(services.sim_info_manager().get(sim_id), op) def save(self, persistence_master_message): if not self._relationships: return persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent relationship_component_data = persistable_data.Extensions[ protocols.PersistableObjectRelationshipComponent.persistable_data] for (key, value) in self._relationships.items(): if not value.persisted_tuning: continue with ProtocolBufferRollback(relationship_component_data. relationships) as relationship_data: relationship_data.sim_id = key relationship_data.value = value.get_value() persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): relationship_component_data = persistable_data.Extensions[ protocols.PersistableObjectRelationshipComponent.persistable_data] for relationship in relationship_component_data.relationships: self.modify_relationship(relationship.sim_id, relationship.value, from_load=True)
class ContentSet(HasTunableFactory): __qualname__ = 'ContentSet' FACTORY_TUNABLES = { 'description': ' \n This is where you tune any sub actions of this interaction.\n \n The interactions here can be tuned as reference to individual\n affordances, lists of affordances, or phase affordances.\n \n Sub actions are affordances that can be run anytime this \n interaction is active. Autonomy will choose which interaction\n runs.\n \n Using phase affordances you can also tune Quick Time or \n optional affordances that can appear.\n ', 'affordance_links': TunableAffordanceLinkList(class_restrictions=('MixerInteraction', )), 'affordance_lists': TunableList(TunableAffordanceListReference()), 'phase_affordances': TunableMapping( description= '\n A mapping of phase names to affordance links and affordance lists. \n \n This is also where you can specify an affordance is Quick Time (or\n an optional affordance) and how many steps are required before an\n option affordance is made available.\n ', value_type=TunableList( TunableTuple(affordance_links=TunableAffordanceLinkList( class_restrictions=('MixerInteraction', )), affordance_lists=TunableList( TunableAffordanceListReference())))), 'phase_tuning': OptionalTunable( TunableTuple( description= '\n When enabled, statistic will be added to target and is used to\n determine the phase index to determine which affordance group to use\n in the phase affordance.\n ', turn_statistic=Statistic.TunableReference( description= '\n The statistic used to track turns during interaction.\n Value will be reset to 0 at the start of each phase.\n ' ), target=TunableEnumEntry( description= '\n The participant the affordance will target.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor))) } EMPTY_LINKS = None def __init__(self, affordance_links, affordance_lists, phase_affordances, phase_tuning): self._affordance_links = affordance_links self._affordance_lists = affordance_lists self.phase_tuning = phase_tuning self._phase_affordance_links = [] for key in sorted(phase_affordances.keys()): self._phase_affordance_links.append(phase_affordances[key]) def _get_all_affordance_for_phase_gen(self, phase_affordances): for affordance in phase_affordances.affordance_links: yield affordance for affordance_list in phase_affordances.affordance_lists: for affordance in affordance_list: yield affordance def all_affordances_gen(self, phase_index=None): if phase_index is not None and self._phase_affordance_links: phase_index = min(phase_index, len(self._phase_affordance_links) - 1) phase = self._phase_affordance_links[phase_index] for phase_affordances in phase: for affordance in self._get_all_affordance_for_phase_gen( phase_affordances): yield affordance else: for phase in self._phase_affordance_links: for phase_affordances in phase: for affordance in self._get_all_affordance_for_phase_gen( phase_affordances): yield affordance for link in self._affordance_links: yield link for l in self._affordance_lists: for link in l: yield link @property def num_phases(self): return len(self._phase_affordance_links) def has_affordances(self): return bool(self._affordance_links) or (bool( self._affordance_lists) or bool(self._phase_affordance_links))
class ObjectInventoryComponent(GetPutComponentMixin, InventoryComponent, component_name=types.INVENTORY_COMPONENT): DEFAULT_OBJECT_INVENTORY_AFFORDANCES = TunableList(TunableReference(description='\n Affordances for all object inventories.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))) FACTORY_TUNABLES = {'description': '\n Generate an object inventory for this object\n ', 'inventory_type': TunableEnumEntry(description='\n Inventory Type must be set for the object type you add this for.\n ', tunable_type=InventoryType, default=InventoryType.UNDEFINED, invalid_enums=(InventoryType.UNDEFINED, InventoryType.SIM)), 'visible': Tunable(description='\n If this inventory is visible to player.', tunable_type=bool, default=True), 'starting_objects': TunableList(description='\n Objects in this list automatically populate the inventory when its\n owner is created. Currently, to keep the game object count down, an\n object will not be added if the object inventory already has\n another object of the same type.', tunable=TunableReference(manager=services.definition_manager(), description='Objects to populate inventory with.', pack_safe=True)), 'purchasable_objects': OptionalTunable(description='\n If this list is enabled, an interaction to buy the purchasable\n objects through a dialog picker will show on the inventory object.\n \n Example usage: a list of books for the bookshelf inventory.\n ', tunable=TunableTuple(show_description=Tunable(description='\n Toggles whether the object description should show in the \n purchase picker.\n ', tunable_type=bool, default=False), objects=TunableList(description='\n A list of object definitions that can be purchased.\n ', tunable=TunableReference(manager=services.definition_manager(), description='')))), 'score_contained_objects_for_autonomy': Tunable(description='\n Whether or not to score for autonomy any objects contained in this object.', tunable_type=bool, default=True), 'item_state_triggers': TunableList(description="\n The state triggers to modify inventory owner's state value based on\n inventory items states.\n ", tunable=ItemStateTrigger.TunableFactory()), 'allow_putdown_in_inventory': Tunable(description="\n This inventory allows Sims to put objects away into it, such as books\n or other carryables. Ex: mailbox has an inventory but we don't want\n Sims putting away items in the inventory.", tunable_type=bool, default=True), 'test_set': OptionalTunable(description='\n If enabled, the ability to pick up items from and put items in this\n object is gated by this test.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('TestSetInstance',))), 'count_statistic': OptionalTunable(description='\n A statistic whose value will be the number of objects in this\n inventory. It will automatically be added to the object owning this\n type of component.\n ', tunable=Statistic.TunableReference()), 'return_owned_objects': Tunable(description="\n If enabled, inventory objects will return to their household\n owner's inventory when this object is destroyed off lot. This is\n because build buy can undo actions on lot and cause object id\n collisions.\n \n We first consider the closest instanced Sims, and finally move to\n the household inventory if we can't move to a Sim's inventory.\n ", tunable_type=bool, default=False), '_use_top_item_tooltip': Tunable(description="\n If checked, this inventory would use the top item's tooltip as its\n own tooltip. \n ", tunable_type=bool, default=False)} def __init__(self, owner, inventory_type, visible, starting_objects, purchasable_objects, score_contained_objects_for_autonomy, item_state_triggers, allow_putdown_in_inventory, test_set, count_statistic, return_owned_objects, _use_top_item_tooltip, **kwargs): super().__init__(owner, **kwargs) self._inventory_type = inventory_type self.visible = visible self.starting_objects = starting_objects self.purchasable_objects = purchasable_objects self.score_contained_objects_for_autonomy = score_contained_objects_for_autonomy self.item_state_triggers = item_state_triggers self.allow_putdown_in_inventory = allow_putdown_in_inventory self.test_set = test_set self.count_statistic = count_statistic self.return_owned_objects = return_owned_objects self._use_top_item_tooltip = _use_top_item_tooltip @property def inventory_type(self): return self._inventory_type @property def default_item_location(self): return ItemLocation.OBJECT_INVENTORY @componentmethod def get_inventory_access_constraint(self, sim, is_put, carry_target, use_owner_as_target_for_resolver=False): if use_owner_as_target_for_resolver: def constraint_resolver(animation_participant, default=None): if animation_participant in (AnimationParticipant.SURFACE, PostureSpecVariable.SURFACE_TARGET, AnimationParticipant.TARGET, PostureSpecVariable.INTERACTION_TARGET): return self.owner return default else: constraint_resolver = None return self._get_access_constraint(sim, is_put, carry_target, resolver=constraint_resolver) @componentmethod def get_inventory_access_animation(self, *args, **kwargs): return self._get_access_animation(*args, **kwargs) @property def should_score_contained_objects_for_autonomy(self): return self.score_contained_objects_for_autonomy @property def use_top_item_tooltip(self): return self._use_top_item_tooltip def _get_inventory_count_statistic(self): return self.count_statistic def on_add(self): for trigger in self.item_state_triggers: self.add_state_trigger(trigger(self)) super().on_add() def on_reset_component_get_interdependent_reset_records(self, reset_reason, reset_records): if reset_reason == ResetReason.BEING_DESTROYED: if not services.current_zone().is_zone_shutting_down: if not self.is_shared_inventory: if self.return_owned_objects: if not self.owner.is_on_active_lot(): household_manager = services.household_manager() objects_to_transfer = list(iter(self)) for obj in objects_to_transfer: household_id = obj.get_household_owner_id() if household_id is not None: household = household_manager.get(household_id) if household is not None: household.move_object_to_sim_or_household_inventory(obj) super().on_reset_component_get_interdependent_reset_records(reset_reason, reset_records) def on_post_bb_fixup(self): self._add_starting_objects() def _add_starting_objects(self): for definition in self.starting_objects: if self.has_item_with_definition(definition): continue new_object = create_object(definition, loc_type=ItemLocation.OBJECT_INVENTORY) if new_object is None: logger.error('Failed to create object {}', definition) else: new_object.set_household_owner_id(self.owner.get_household_owner_id()) if not self.player_try_add_object(new_object): logger.error('Failed to add object {} to inventory {}', new_object, self) new_object.destroy(source=self.owner, cause='Failed to add starting object to inventory.') def component_interactable_gen(self): yield self def component_super_affordances_gen(self, **kwargs): if self.visible: for affordance in self.DEFAULT_OBJECT_INVENTORY_AFFORDANCES: yield affordance def _can_access(self, sim): if self.test_set is not None: resolver = DoubleObjectResolver(sim, self.owner) result = self.test_set(resolver) if not result: return False return True @componentmethod def can_access_for_pickup(self, sim): if not self._can_access(sim): return False elif any(self.owner.state_value_active(value) for value in InventoryTuning.INVALID_ACCESS_STATES): return False return True @componentmethod def can_access_for_putdown(self, sim): if not self.allow_putdown_in_inventory: return False elif not self._can_access(sim): return False return True
class CraftingTuning: __qualname__ = 'CraftingTuning' STATIC_CRAFTING_COMMODITY = StaticCommodity.TunableReference( description= '\n The static commodity all interactions used in recipes must be tagged\n with.\n ' ) TURN_STATISTIC = Statistic.TunableReference( description= '\n The statistic used to track turns during a crafting process. Value will\n be reset to 0 at the start of each phase.\n ' ) MAX_TURNS_FOR_AUTOSMOKE = TunableRange( description= '\n The maximum number of turns a phase should take during the autosmoke.\n ', tunable_type=int, default=2, minimum=2) PROGRESS_STATISTIC = TunableReference( description= '\n The statistic used to track crafting progress during a crafting process.\n Recipes using this are complete when the stat maxes out.\n ', manager=get_instance_manager(sims4.resources.Types.STATISTIC)) PROGRESS_VIRTUAL_TURNS = Tunable( description= '\n When a phase is progress-based, this controlls how many turns it appears\n to have in the crafting quality UI.\n ', tunable_type=int, default=10) SERVINGS_STATISTIC = Statistic.TunableReference( description='\n The statistic to link to the servings.\n ' ) STATE_EFFECT_MAP = TunableMapping( description= '\n A mapping of states to effects that take place when advancing\n phases.\n ', key_type=TunableStateValueReference( description= '\n A state value. The Object of the interaction will be considered\n first. If the state is not present, the ActorSurface will be\n considered.\n ' ), value_type=TunableReference( description= '\n Actions to apply if the specified state is enabled when advancing\n phases.\n ', manager=get_instance_manager(sims4.resources.Types.ACTION))) INSUFFICIENT_FUNDS_TOOLTIP = TunableLocalizedStringFactory( description= '\n Grayed-out tooltip message when sim lacks sufficient funds.\n ', default=1906305656) QUALITY_STATE = CommodityBasedObjectState.TunableReference( description= '\n The statistic used to track quality during a crafting process.\n ' ) CONSUMABLE_STATE = CommodityBasedObjectState.TunableReference( description= '\n The statistic used to track consumed state during a crafting process.\n ' ) FRESHNESS_STATE = CommodityBasedObjectState.TunableReference( description= '\n The object state used to track freshness.\n ') SPOILED_STATE_VALUE = TunableStateValueReference( description= '\n The object state used to track freshness.\n ', class_restrictions=CommodityBasedObjectStateValue) SPOILED_STRING = TunableLocalizedString( description='\n The spoiled object string.\n ') CONSUMABLE_EMPTY_STATE_VALUE = TunableStateValueReference( description= "\n The object state value for empty consumable. Empty consumable doesn't have hovertip.\n ", class_restrictions=CommodityBasedObjectStateValue) LOCK_FRESHNESS_STATE_VALUE = TunableStateValueReference( description= '\n Does this object have a lock freshness state value\n ', class_restrictions=ObjectStateValue) MASTERWORK_STATE = ObjectState.TunableReference( description= '\n The object state used to track if this is a masterwork or not.\n ' ) MASTERWORK_STATE_VALUE = TunableStateValueReference( description= '\n The masterwork state value, as opposed to the normal work state value.\n ' ) QUALITY_STATE_VALUE_MAP = TunableMapping( description= '\n The quality mapping to the UI numbers.\n ', key_type=TunableStateValueReference( description='\n The quality state values.\n ' ), value_type=TunableTuple( state_star_number=Tunable( description= '\n The number of stars shows in UI.\n ', tunable_type=int, default=0), state_string=TunableLocalizedStringFactory( description= '\n The quality state string in Crafting Inspector.\n ' ))) CONSUMABLE_STATE_VALUE_MAP = TunableMapping( description= '\n The consumable state mapping to the UI numbers.\n ', key_type=TunableStateValueReference( description= '\n The consumable state values.\n '), value_type=TunableLocalizedString( description= '\n The consumable state string in consumable tooltip UI.\n ' )) DEFAULT_RESUME_AFFORDANCE = TunableReference( description= '\n The affordance that is run when choosing to resume a crafting process.\n ', manager=get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('CraftingResumeInteraction', )) SHARED_FRIDGE_INVENTORY_TYPE = TunableEnumEntry( description= '\n Type of inventory used by the fridge objects.\n ', tunable_type=InventoryType, default=InventoryType.UNDEFINED) @classproperty def QUALITY_STATISTIC(cls): return cls.QUALITY_STATE.linked_stat @classproperty def CONSUME_STATISTIC(cls): return cls.CONSUMABLE_STATE.linked_stat @classproperty def FRESHNESS_STATISTIC(cls): raise RuntimeError( '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.' ) return cls.FRESHNESS_STATE.linked_stat @classmethod def get_quality_state_value(cls, stat_type, quality_stat_value): for (quality_state, value) in cls.QUALITY_STATE_VALUE_MAP.items(): while quality_state.state is not None and quality_state.state.linked_stat is stat_type: if quality_stat_value >= quality_state.range.lower_bound and quality_stat_value < quality_state.range.upper_bound: return value
class Puddle(objects.game_object.GameObject): WEED_DEFINITIONS = TunableDefinitionList(description='\n Possible weed objects which can be spawned by evaporation.') PLANT_DEFINITIONS = TunableDefinitionList(description='\n Possible plant objects which can be spawned by evaporation.') INSTANCE_TUNABLES = {'indoor_evaporation_time': TunableInterval(description='\n Number of SimMinutes this puddle should take to evaporate when \n created indoors.\n ', tunable_type=TunableSimMinute, default_lower=200, default_upper=300, minimum=1, tuning_group=GroupNames.DEPRECATED), 'outdoor_evaporation_time': TunableInterval(description='\n Number of SimMinutes this puddle should take to evaporate when \n created outdoors.\n ', tunable_type=TunableSimMinute, default_lower=30, default_upper=60, minimum=1, tuning_group=GroupNames.DEPRECATED), 'evaporation_outcome': TunableTuple(nothing=TunableRange(int, 5, minimum=1, description='Relative chance of nothing.'), weeds=TunableRange(int, 2, minimum=0, description='Relative chance of weeds.'), plant=TunableRange(int, 1, minimum=0, description='Relative chance of plant.'), tuning_group=GroupNames.PUDDLES), 'intial_stat_value': TunableTuple(description='\n This is the starting value for the stat specified. This controls \n how long it takes to mop this puddle.\n ', stat=Statistic.TunableReference(description='\n The stat used for mopping puddles.\n '), value=Tunable(description='\n The initial value this puddle should have for the mopping stat.\n The lower the value (-100,100), the longer it takes to mop up.\n ', tunable_type=int, default=-20), tuning_group=GroupNames.PUDDLES), 'evaporation_data': TunableTuple(description='\n This is the information for evaporation. This controls how long this\n puddle takes to evaporate.\n ', commodity=Commodity.TunableReference(description='\n The commodity used for evaporation.\n '), initial_value=TunableInterval(description='\n Initial value of this commodity. Time it takes to evaporate\n will be based on how fast this commodity decays.\n (Based on loot given in weather aware component)\n ', tunable_type=float, default_lower=30, default_upper=60, minimum=1), tuning_group=GroupNames.PUDDLES), 'puddle_liquid': TunableEnumEntry(description='\n The liquid that the puddle is made of.\n ', tunable_type=PuddleLiquid, default=PuddleLiquid.INVALID, invalid_enums=(PuddleLiquid.INVALID,), tuning_group=GroupNames.PUDDLES), 'puddle_size': TunableEnumEntry(description='\n The size of the puddle.\n ', tunable_type=PuddleSize, default=PuddleSize.NoPuddle, invalid_enums=(PuddleSize.NoPuddle,), tuning_group=GroupNames.PUDDLES), 'puddle_grow_chance': TunableMultiplier.TunableFactory(description='\n The chance of puddle to grow.\n ', tuning_group=GroupNames.PUDDLES)} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._evaporate_callback_handle = None self.statistic_tracker.set_value(self.intial_stat_value.stat, self.intial_stat_value.value) @property def size_count(self): if self.puddle_size == PuddleSize.SmallPuddle: return 1 if self.puddle_size == PuddleSize.MediumPuddle: return 2 elif self.puddle_size == PuddleSize.LargePuddle: return 3 def place_puddle(self, target, max_distance, ids_to_ignore=DEFAULT): destroy_puddle = True try: if ids_to_ignore is DEFAULT: ids_to_ignore = (self.id,) else: ids_to_ignore.append(self.id) flags = placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_POSITIONS flags = flags | placement.FGLSearchFlag.ALLOW_GOALS_IN_SIM_INTENDED_POSITIONS flags = flags | placement.FGLSearchFlag.STAY_IN_SAME_CONNECTIVITY_GROUP if target.is_on_active_lot(): flags = flags | placement.FGLSearchFlag.SHOULD_TEST_BUILDBUY else: flags = flags | placement.FGLSearchFlag.SHOULD_TEST_ROUTING flags = flags | placement.FGLSearchFlag.USE_SIM_FOOTPRINT flags = flags | placement.FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS flags = flags | placement.FGLSearchFlag.DONE_ON_MAX_RESULTS radius_target = target while radius_target.parent is not None: radius_target = radius_target.parent if radius_target.is_part: radius_target = radius_target.part_owner routing_surface = target.routing_surface routing_surface = SurfaceIdentifier(routing_surface.primary_id, routing_surface.secondary_id, SurfaceType.SURFACETYPE_WORLD) starting_location = placement.create_starting_location(position=target.position + target.forward*radius_target.object_radius, orientation=sims4.random.random_orientation(), routing_surface=routing_surface) fgl_context = placement.create_fgl_context_for_object(starting_location, self, search_flags=flags, ignored_object_ids=ids_to_ignore, max_distance=max_distance) (position, orientation) = placement.find_good_location(fgl_context) if position is not None: destroy_puddle = False self.place_puddle_at(position, orientation, routing_surface) return True return False finally: if destroy_puddle: self.destroy(source=self, cause='Failed to place puddle.') def place_puddle_at(self, position, orientation, routing_surface): self.location = sims4.math.Location(sims4.math.Transform(position, orientation), routing_surface) self.fade_in() self.start_evaporation() def try_grow_puddle(self): if self.puddle_size == PuddleSize.LargePuddle: return resolver = SingleObjectResolver(self) chance = self.puddle_grow_chance.get_multiplier(resolver) if random.random() > chance: return else: if self.puddle_size == PuddleSize.MediumPuddle: puddle = create_puddle(PuddleSize.LargePuddle, puddle_liquid=self.puddle_liquid) else: puddle = create_puddle(PuddleSize.MediumPuddle, puddle_liquid=self.puddle_liquid) if puddle.place_puddle(self, 1, ids_to_ignore=[self.id]): if self._evaporate_callback_handle is not None: self.commodity_tracker.remove_listener(self._evaporate_callback_handle) self.destroy(self, cause='Puddle is growing.', fade_duration=ClientObjectMixin.FADE_DURATION) return puddle def start_evaporation(self): tracker = self.commodity_tracker tracker.set_value(self.evaporation_data.commodity, self.evaporation_data.initial_value.random_float()) if self._evaporate_callback_handle is not None: tracker.remove_listener(self._evaporate_callback_handle) threshold = sims4.math.Threshold(0.0, operator.le) self._evaporate_callback_handle = tracker.create_and_add_listener(self.evaporation_data.commodity, threshold, self.evaporate) def evaporate(self, stat_instance): if self.in_use: self.start_evaporation() return if self._evaporate_callback_handle is not None: self.commodity_tracker.remove_listener(self._evaporate_callback_handle) self._evaporate_callback_handle = None if self.is_on_natural_ground(): defs_to_make = sims4.random.weighted_random_item([(self.evaporation_outcome.nothing, None), (self.evaporation_outcome.weeds, self.WEED_DEFINITIONS), (self.evaporation_outcome.plant, self.PLANT_DEFINITIONS)]) if defs_to_make: def_to_make = random.choice(defs_to_make) obj_location = sims4.math.Location(sims4.math.Transform(self.position, sims4.random.random_orientation()), self.routing_surface) (result, _) = build_buy.test_location_for_object(None, def_to_make.id, obj_location, [self]) if result: obj = objects.system.create_object(def_to_make) obj.opacity = 0 obj.location = self.location obj.fade_in() self.destroy(self, cause='Puddle is evaporating.', fade_duration=ClientObjectMixin.FADE_DURATION) def load_object(self, object_data, **kwargs): super().load_object(object_data, **kwargs) self.start_evaporation()