class RetailCustomerSituation(BusinessSituationMixin, SituationComplexCommon): INSTANCE_TUNABLES = { 'customer_job': SituationJob.TunableReference( description= '\n The situation job for the customer.\n '), 'role_state_go_to_store': RoleState.TunableReference( description= '\n The role state for getting the customer inside the store. This is\n the default role state and will be run first before any other role\n state can start.\n ' ), 'role_state_browse': OptionalTunable( description= '\n If enabled, the customer will be able to browse items.\n ', tunable=TunableTuple( role_state=RoleState.TunableReference( description= '\n The role state for the customer browsing items.\n ' ), browse_time_min=TunableSimMinute( description= '\n The minimum amount of time, in sim minutes, the customer\n will browse before moving on to the next state. When the\n customer begins browsing, a random time will be chosen\n between the min and max browse time.\n ', default=10), browse_time_max=TunableSimMinute( description= '\n The maximum amount of time, in sim minutes, the customer\n will browse before moving on to the next state. When the\n customer begins browsing, a random time will be chosen\n between the min and max browse time.\n ', default=20), browse_time_extension_tunables=OptionalTunable( TunableTuple( description= '\n A set of tunables related to browse time extensions.\n ', extension_perk=TunableReference( description= '\n Reference to a perk that, if unlocked, will increase\n browse time by a set amount.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), time_extension=TunableSimMinute( description= '\n The amount of time, in Sim minutes, that browse time\n will be increased by if the specified "extension_perk"\n is unlocked.\n ', default=30))))), 'role_state_buy': OptionalTunable( description= '\n If enabled, the customer will be able to buy items.\n ', tunable=TunableTuple( role_state=RoleState.TunableReference( description= '\n The role state for the customer buying items.\n ' ), price_range=TunableInterval( description= '\n The minimum and maximum price of items this customer will\n buy.\n ', tunable_type=int, default_lower=1, default_upper=100, minimum=1))), 'role_state_loiter': RoleState.TunableReference( description= '\n The role state for the customer loitering. If Buy Role State and\n Browse Role State are both disabled, the Sim will fall back to\n loitering until Total Shop Time runs out.\n ' ), 'go_to_store_interaction': TunableInteractionOfInterest( description= '\n The interaction that, when run by a customer, will switch the\n situation state to start browsing, buying, or loitering.\n ' ), 'total_shop_time_max': TunableSimMinute( description= "\n The maximum amount of time, in sim minutes, a customer will shop.\n This time starts when they enter the store. At the end of this\n time, they'll finish up whatever their current interaction is and\n leave.\n ", default=30), 'total_shop_time_min': TunableSimMinute( description= "\n The minimum amount of time, in sim minutes, a customer will shop.\n This time starts when they enter the store. At the end of this\n time, they'll finish up whatever their current interaction is and\n leave.\n ", default=1), 'buy_interaction': TunableInteractionOfInterest( description= '\n The interaction that, when run by a customer, buys an object.\n ' ), 'initial_purchase_intent': TunableInterval( description= "\n The customer's purchase intent statistic is initialized to a random\n value in this interval when they enter the store.\n ", tunable_type=int, default_lower=0, default_upper=100), 'purchase_intent_extension_tunables': OptionalTunable( TunableTuple( description= '\n A set of tunables related to purchase intent extensions.\n ', extension_perk=TunableReference( description= '\n Reference to a perk that, if unlocked, will increase purchase\n intent by a set amount.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), purchase_intent_extension=TunableRange( description= '\n The amount to increase the base purchase intent statistic by if\n the specified "extension_perk" is unlocked.\n ', tunable_type=int, default=5, minimum=0, maximum=100))), 'purchase_intent_empty_notification': TunableUiDialogNotificationSnippet( description= '\n Notification shown by customer when purchase intent hits bottom and\n the customer leaves.\n ' ), 'nothing_in_price_range_notification': TunableUiDialogNotificationSnippet( description= "\n Notification shown by customers who are ready to buy but can't find\n anything in their price range.\n " ), '_situation_start_tests': TunableCustomerSituationInitiationSet( description= '\n A set of tests that will be run when determining if this situation\n can be chosen to start. \n ' ) } CONTINUE_SHOPPING_THRESHOLD = TunableSimMinute( description= "\n If the customer has this much time or more left in their total shop\n time, they'll start the browse/buy process over again after purchasing\n something. If they don't have this much time remaining, they'll quit\n shopping.\n ", default=30) PRICE_RANGE = TunableTuple( description= '\n Statistics that are set to the min and max price range statistics.\n These are automatically added to the customer in this situation and\n will be updated accordingly.\n \n The stats should not be persisted -- the situation will readd them\n on load.\n ', min=Statistic.TunablePackSafeReference(), max=Statistic.TunablePackSafeReference()) PURCHASE_INTENT_STATISTIC = Statistic.TunablePackSafeReference( description= "\n A statistic added to customers that track their intent to purchase\n something. At the minimum value they will leave, and at max value they\n will immediately try to buy something. Somewhere in between, there's a\n chance for them to not buy something when they go to the buy state.\n " ) PURCHASE_INTENT_CHANCE_CURVE = TunableCurve( description= '\n A mapping of Purchase Intent Statistic value to the chance (0-1) that\n the customer will buy something during the buy state.\n ', x_axis_name='Purchase Intent', y_axis_name='Chance') REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES @classmethod def can_start_situation(cls, resolver): return cls._situation_start_tests.run_tests(resolver) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._customer = None self._showing_purchase_intent = False reader = self._seed.custom_init_params_reader if reader is None: self._saved_purchase_intent = None else: self._saved_purchase_intent = reader.read_int64( 'purchase_intent', None) self._min_price_range_multiplier = 1 self._max_price_range_multiplier = 1 self._total_shop_time_multiplier = 1 self._purchase_intent_watcher_handle = None def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self._customer is not None: purchase_intent = self._customer.get_stat_value( self.PURCHASE_INTENT_STATISTIC) writer.write_int64('purchase_intent', int(purchase_intent)) @classmethod def _states(cls): return (SituationStateData(1, _GoToStoreState), SituationStateData(2, _BrowseState), SituationStateData(3, _BuyState), SituationStateData(4, _LoiterState)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.customer_job, cls.role_state_go_to_store)] @classmethod def default_job(cls): return cls.customer_job def start_situation(self): super().start_situation() self._change_state(_GoToStoreState()) @classmethod def get_sims_expected_to_be_in_situation(cls): return 1 @classproperty def situation_serialization_option(cls): return situations.situation_types.SituationSerializationOption.LOT def validate_customer(self, sim_info): if self._customer is None: return False return self._customer.sim_info is sim_info def _on_set_sim_job(self, sim, job_type): super()._on_set_sim_job(sim, job_type) self._customer = sim self._update_price_range_statistics() self._initialize_purchase_intent() def _on_remove_sim_from_situation(self, sim): sim_job = self.get_current_job_for_sim(sim) super()._on_remove_sim_from_situation(sim) self._remove_purchase_intent() self._customer = None services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist( sim.id, sim_job) self._self_destruct() def _situation_timed_out(self, *args, **kwargs): if not isinstance(self._cur_state, _BuyState): super()._situation_timed_out(*args, **kwargs) def adjust_browse_time(self, multiplier): if type(self._cur_state) is _BrowseState: self._cur_state.adjust_timeout(multiplier) def adjust_total_shop_time(self, multiplier): if multiplier == 0: self._self_destruct() elif type(self._cur_state) is _GoToStoreState: self._total_shop_time_multiplier *= multiplier else: remaining_minutes = self._get_remaining_time_in_minutes() remaining_minutes *= multiplier self.change_duration(remaining_minutes) def adjust_price_range(self, min_multiplier=1, max_multiplier=1): if self.role_state_buy is None: return self._min_price_range_multiplier *= min_multiplier self._max_price_range_multiplier *= max_multiplier self._update_price_range_statistics() def _update_price_range_statistics(self): (min_price, max_price) = self._get_min_max_price_range() if self.PRICE_RANGE.min is not None: min_stat = self._customer.get_statistic(self.PRICE_RANGE.min) min_stat.set_value(min_price) if self.PRICE_RANGE.max is not None: max_stat = self._customer.get_statistic(self.PRICE_RANGE.max) max_stat.set_value(max_price) def _get_min_max_price_range(self): price_range = self.role_state_buy.price_range return (max(0, price_range.lower_bound * self._min_price_range_multiplier), max(1, price_range.upper_bound * self._max_price_range_multiplier)) def _initialize_purchase_intent(self): if self.role_state_buy is None: return if self._saved_purchase_intent is None: purchase_intent = random.randint( self.initial_purchase_intent.lower_bound, self.initial_purchase_intent.upper_bound) if self.purchase_intent_extension_tunables is not None: active_household = services.active_household() if active_household is not None: if active_household.bucks_tracker.is_perk_unlocked( self.purchase_intent_extension_tunables. extension_perk): purchase_intent += self.purchase_intent_extension_tunables.purchase_intent_extension purchase_intent = sims4.math.clamp( self.PURCHASE_INTENT_STATISTIC.min_value + 1, purchase_intent, self.PURCHASE_INTENT_STATISTIC.max_value - 1) else: purchase_intent = self._saved_purchase_intent tracker = self._customer.get_tracker(self.PURCHASE_INTENT_STATISTIC) tracker.set_value(self.PURCHASE_INTENT_STATISTIC, purchase_intent, add=True) self._purchase_intent_watcher_handle = tracker.add_watcher( self._purchase_intent_watcher) if self._on_social_group_changed not in self._customer.on_social_group_changed: self._customer.on_social_group_changed.append( self._on_social_group_changed) def _remove_purchase_intent(self): if self._customer is not None: if self._purchase_intent_watcher_handle is not None: tracker = self._customer.get_tracker( self.PURCHASE_INTENT_STATISTIC) tracker.remove_watcher(self._purchase_intent_watcher_handle) self._purchase_intent_watcher_handle = None tracker.remove_statistic(self.PURCHASE_INTENT_STATISTIC) if self._on_social_group_changed in self._customer.on_social_group_changed: self._customer.on_social_group_changed.remove( self._on_social_group_changed) self._set_purchase_intent_visibility(False) def _on_social_group_changed(self, sim, group): if self._customer in group: if self._on_social_group_members_changed not in group.on_group_changed: group.on_group_changed.append( self._on_social_group_members_changed) elif self._on_social_group_members_changed in group.on_group_changed: group.on_group_changed.remove( self._on_social_group_members_changed) def _on_social_group_members_changed(self, group): if self._customer is not None: employee_still_in_group = False business_manager = services.business_service( ).get_business_manager_for_zone() if self._customer in group: for sim in group: if not business_manager.is_household_owner( sim.household_id): if business_manager.is_employee(sim.sim_info): employee_still_in_group = True break employee_still_in_group = True break if employee_still_in_group: self._set_purchase_intent_visibility(True) else: self._set_purchase_intent_visibility(False) def on_sim_reset(self, sim): super().on_sim_reset(sim) if isinstance(self._cur_state, _BuyState) and self._customer is sim: new_buy_state = _BuyState() new_buy_state.object_id = self._cur_state.object_id self._change_state(new_buy_state) def _set_purchase_intent_visibility(self, toggle): if self._showing_purchase_intent is not toggle and ( not toggle or isinstance(self._cur_state, _BrowseState)): self._showing_purchase_intent = toggle stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC, add=False) if stat is not None: value = stat.get_value() self._send_purchase_intent_message(stat.stat_type, value, value, toggle) def _purchase_intent_watcher(self, stat_type, old_value, new_value): if stat_type is not self.PURCHASE_INTENT_STATISTIC: return self._send_purchase_intent_message(stat_type, old_value, new_value, self._showing_purchase_intent) if new_value == self.PURCHASE_INTENT_STATISTIC.max_value: self._on_purchase_intent_max() elif new_value == self.PURCHASE_INTENT_STATISTIC.min_value: self._on_purchase_intent_min() def _send_purchase_intent_message(self, stat_type, old_value, new_value, toggle): business_manager = services.business_service( ).get_business_manager_for_zone() if business_manager is not None and business_manager.is_owner_household_active: op = PurchaseIntentUpdate( self._customer.sim_id, stat_type.convert_to_normalized_value(old_value), stat_type.convert_to_normalized_value(new_value), toggle) distributor.system.Distributor.instance().add_op( self._customer, op) def _on_purchase_intent_max(self): if isinstance(self._cur_state, _BuyState): return if isinstance(self._cur_state, _GoToStoreState): self._set_shop_duration() self._change_state(_BuyState()) def _on_purchase_intent_min(self): resolver = SingleSimResolver(self._customer) dialog = self.purchase_intent_empty_notification( self._customer, resolver) dialog.show_dialog() self._self_destruct() def _choose_starting_state(self): if self.role_state_browse is not None: return _BrowseState() if self.role_state_buy is not None: return _BuyState() return _LoiterState() def _choose_post_browse_state(self): if self._customer is None: return if self.role_state_buy is not None: stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC, add=False) if stat is not None: value = stat.get_value() chance = self.PURCHASE_INTENT_CHANCE_CURVE.get(value) if random.random() > chance: return _BrowseState() self._set_purchase_intent_visibility(False) return _BuyState() return _LoiterState() def _choose_post_buy_state(self): minutes_remaining = self._get_remaining_time_in_minutes() if minutes_remaining < self.CONTINUE_SHOPPING_THRESHOLD: return if self.role_state_browse is not None: return _BrowseState() return _LoiterState() def _set_shop_duration(self): shop_time = random.randint(self.total_shop_time_min, self.total_shop_time_max) shop_time *= self._total_shop_time_multiplier self.change_duration(shop_time)
class StatisticStaticModifier(HasTunableSingletonFactory, BaseGameEffectModifier): FACTORY_TUNABLES = { 'statistic': Statistic.TunablePackSafeReference( description='\n "The statistic we are operating on.'), 'modifier': TunableVariant( description= '\n How we want to modify the statistic. \n ', ceiling=TunableTuple( description= '\n Cap the value at the specified number.\n ', number=Tunable( description= '\n The number to cap the value at. Can be negative.\n ', tunable_type=int, default=0), priority=TunableRange( description= '\n The priority in which to apply the modifier. Higher are later\n ', tunable_type=int, default=2, minimum=2), locked_args={'option': StatisticStaticModifierOption.CEILING}), floor=TunableTuple( description= '\n floor the value at the specified number.\n ', number=Tunable( description= '\n The number to floor the value at. Can be negative.\n ', tunable_type=int, default=0), priority=TunableRange( description= '\n The priority in which to apply the modifier. Higher are later\n ', tunable_type=int, default=2, minimum=2), locked_args={'option': StatisticStaticModifierOption.FLOOR}), delta=TunableTuple( description= '\n Modify the value by the specified number.\n ', number=Tunable( description= '\n The number to modify the value by. Can be negative.\n ', tunable_type=int, default=0), locked_args={ 'option': StatisticStaticModifierOption.DELTA, 'priority': 0 }), normalize=TunableTuple( description= '\n Normalize (i.e. move towards default) the value by the specified number.\n ', number=Tunable( description= '\n The number to modify the value by. Can be negative.\n ', tunable_type=int, default=0), locked_args={ 'option': StatisticStaticModifierOption.NORMALIZE, 'priority': 1 }), default='delta') } def __init__(self, statistic, modifier, **kwargs): super().__init__(GameEffectType.STATISTIC_STATIC_MODIFIER) self._statistic = statistic self._option = modifier.option self._number = modifier.number self.priority = modifier.priority def apply_modifier(self, sim_info): if self._statistic is None: return stat = sim_info.get_statistic(self._statistic, add=True) if stat is None: if sim_info.lod != SimInfoLODLevel.MINIMUM: logger.warn( 'Unable to add statistic: {} to sim: {} for statistic_static_modifier. Perhaps statistic min lod value should be lower', self._statistic, sim_info) return stat.add_statistic_static_modifier(self) def remove_modifier(self, sim_info, handle): if self._statistic is None: return stat = sim_info.get_statistic(self._statistic) if stat is None: return stat.remove_statistic_static_modifier(self) def apply(self, value, default_value): if self._option == StatisticStaticModifierOption.NORMALIZE: if value > default_value: value -= self._number if value < default_value: value = default_value else: value += default_value if value > default_value: value = default_value return value if self._option == StatisticStaticModifierOption.DELTA: return value + self._number if self._option == StatisticStaticModifierOption.CEILING: if value > self._number: value = self._number return value else: if value < self._number: value = self._number return value