コード例 #1
0
    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]
コード例 #2
0
 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)
コード例 #3
0
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)))))
    }
コード例 #4
0
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
コード例 #5
0
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
        })
コード例 #6
0
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)
コード例 #7
0
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)
コード例 #8
0
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)
コード例 #9
0
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))
コード例 #10
0
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
コード例 #11
0
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
コード例 #12
0
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()