#!/usr/bin/env python # Emacs environement # (setq python-shell-interpreter "/Users/howard/src/moss-n-puddles/.venv/bin/ipython") """ Pets Create interactive 'pets' that consume some sort of food, and need to be 'fed'. Each level of pet requires more aspects for interaction. """ from enum import Enum from re import sub from time import time import random from evennia import TICKER_HANDLER from typeclasses.objects import Object from typeclasses.characters import Character # from typeclasses.lightables import LightSource from commands.feedables import CmdFeedSet from utils.word_list import squish, choices class Hunger(Enum): "States of being for a Pet." RAVENOUS = 100 HUNGRY = 300 FED = 850 FULL = 1000 class Pet(Object): """ This is a base class for Pets. """ fullname = None pers_pronoun = "it" # or he/she/they poss_pronoun = "its" # or his/her/them # Feed about once a day: hungry_rate = -.7 hunger_states = { Hunger.RAVENOUS: [ "looks crazy with hunger", ], Hunger.HUNGRY: [ "looks hungry", "seems hungry", ], Hunger.FED: "looks well", Hunger.FULL: [ "looks content", "looks sated", ] } def at_object_creation(self): """ Called when object is first created. We set up a ticker to update hunger levels regularly. """ self.cmdset.add_default(CmdFeedSet) self.db.hunger_level = Hunger.FULL.value # subscribe ourselves to a ticker to repeatedly call the hook # "update_weather" on this object. The interval is randomized # so as to not have all weather rooms update at the same time. self.db.interval = random.randint(70, 90) TICKER_HANDLER.add(interval=self.db.interval, callback=self.update_state) super().at_object_creation() def hunger(self): if self.db.hunger_level < Hunger.RAVENOUS.value: return Hunger.RAVENOUS if self.db.hunger_level < Hunger.HUNGRY.value: return Hunger.HUNGRY if self.db.hunger_level < Hunger.FED.value: return Hunger.FED return Hunger.FULL def hunger_appearance(self): msgs = self.hunger_states.get(self.hunger()) if isinstance(msgs, str): return f"{self.fullname or self.pers_pronoun} {msgs}" return f"{self.fullname or self.pers_pronoun} {random.choice(msgs)}" def update_state(self, *args, **kwargs): """ Called by the tickerhandler at regular intervals. Even so, we only update at a hungry_rate of the time, picking a random weather message when we do. The tickerhandler requires that this hook accepts any arguments and keyword arguments (hence the *args, **kwargs even though we don't actually use them in this example) """ curr = self.hunger() amount = kwargs.get('amount', self.hungry_rate) self.db.hunger_level = max(self.db.hunger_level + amount, 0) if self.hunger() != curr: self.location.msg_contents(f"|w{self.hunger_appearance()}|n\n") def return_appearance(self, looker, **kwargs): """ This formats the description of this object based on 'hunger'. Called by the 'look' command. Args: looker (Object): Object doing the looking. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). """ return f"{self.db.desc} {self.hunger_appearance()}" def feed(self, giver, obj=None): pass # ------------------------------------------------------------ # Fireplace, both a pet that is hungry and consumes food, # but also emits light when it isn't starving. # ------------------------------------------------------------ class Fire(Pet): """ Fire in this world, is a cute fireplace pet. """ fullname = "The fire in the fireplace" hungry_rate = -5 hunger_states = { Hunger.RAVENOUS: [ "is out. It is dark.", ], Hunger.HUNGRY: [ "shows glowing red embers that seems breathe in a memorizing way.", "is little more than glowing embers casting shadows on the wall.", ], Hunger.FED: [ "dances and shimmies with warmth and light.", "crackles and pops with warmth and light." ], Hunger.FULL: [ "roars to life, casting a bright light around the room.", "burns brightly and is very hot. You feel the need to move back a bit.", ] } def feed_msg(self, giver): now = time() last_fed = giver.ndb.fire_last_fed # could be None if last_fed and (now - last_fed < 30): adj = "some more" else: adj = "some" giver.ndb.fire_last_fed = now get_up = "" gets_up = "" if giver.db.is_sitting: get_up = "get up and" gets_up = "gets up and" if self.db.hunger_level < 5: giver.msg(squish(f"You {get_up} put {adj} wood in the " f"fireplace, and start a fire.")) self.location.msg_contents(squish(f"{giver.name} {gets_up} starts a fire."), exclude=giver) else: giver.msg(squish(f"You {get_up} put {adj} wood on the " f"fire in the fireplace.")) self.location.msg_contents(squish(f"{giver.name} {gets_up} puts {adj} wood on the fire."), exclude=giver) def feed(self, giver, obj=None): """ Feed the fire the default object, wood. """ self.feed_msg(giver) self.update_state(giver=giver, amount=300) def at_pre_object_receive(self, moved_obj, giver, move_type="move", **kwargs): "Burn something in the fireplace." if moved_obj.is_typeclass("typeclasses.things.Wood"): self.feed_msg(giver) self.update_state(giver=giver, amount=400) else: giver.msg(f"You throw {moved_obj.name} in the fireplace, destroying it.") return True # ---------------------------------------------------------------------- # Friendly class Reaction(Enum): SCARED = 1 CONCERNED = 100 INTERESTED = 300 FRIENDLY = 850 ECSTATIC = 1000 class Friendly(Pet): """ This pet keeps track of the characters in the game. It has different reactions based on the characters in the room. """ def at_object_creation(self): """ Called when object is first created. """ super().at_object_creation() # The higher this value the more "spammy" a pet is in making # comments in the room: self.db.active_amount = 3 # If set to 'ignores', the pet is more concerned about the # 'friendliest' character in Room, otherwise, it is more # scared at the stranger: self.db.new_character_reaction = "ignores" @property def friendly_var(self): """ Return variable name on character types use to gauge the reaction of this pet towards that character. """ key = self.key.replace(" ", "_") return f"{key}_friendly_level" def friendly_level(self, character): """ Return reaction level of this pet towards a character. """ varname = self.friendly_var return character.attributes.get(varname) or 0 def friendly_reaction(self, character=None): """ Return reaction enum of this pet towards a character. If character not given, then looks at all characters in the room """ if character: level = self.friendly_level(character) if level < Reaction.SCARED.value: return Reaction.SCARED if level < Reaction.CONCERNED.value: return Reaction.CONCERNED if level < Reaction.INTERESTED.value: return Reaction.INTERESTED if level < Reaction.FRIENDLY.value: return Reaction.FRIENDLY return Reaction.ECSTATIC else: if self.db.new_character_reaction == "ignores": return self.highest_friendly_reaction() return self.lowest_friendly_reaction() @property def local_characters(self): """ Return a list of all Characters in the room with the Pet. """ return [c for c in self.location.contents if c.is_typeclass("typeclasses.characters.Character")] def lowest_friendly_reaction(self): """ Return reaction of this pet to the least friendliest character(s) in the area (room) that this pet resides. Returns a tuple, Reaction and a list of characters. """ # State is a tuple of the level and the character: level = Reaction.ECSTATIC characters = [] for c in self.local_characters: this_level = self.friendly_reaction(c) if this_level.value < level.value: level = this_level characters = [c] if this_level.value == level.value: characters += [c] return (level, characters) def highest_friendly_reaction(self): """ Return reaction of this pet to the friendliest character(s) in the area (room) that this pet resides. Returns a tuple, Reaction and a list of characters. """ # State is a tuple of the level and the character: level = Reaction.SCARED characters = [] for c in self.local_characters: this_level = self.friendly_reaction(c) if this_level.value > level.value: level = this_level characters = [c] if this_level.value == level.value: characters += [c] return (level, characters) def adjust_all(self, amount): """ Adjusts reaction level to all characters that have interacted with this pet, whether they are near it or not. This is essentially a loneliness measure, for out-of-sight, out-of-mind. """ for c in Character.objects.get_objs_with_attr(self.friendly_var): self.adjust_character(c, amount) def adjust_all_locally(self, amount): """ Adjusts reaction level to all characters in the room with this pet. Hanging out with the pet should be helpful. """ for c in self.local_characters: self.adjust_character(c, amount) def adjust_character(self, character, amount): """ Adjusts the reaction to 'character' by an 'amount. Note that this should never go below zero. """ new_val = self.friendly_level(character) + amount if new_val < 0: new_val = 0 character.attributes.add(self.friendly_var, new_val) def return_appearance(self, looker, **kwargs): """ Called by the 'look' command. This formats the description of this object based on 'reaction', and the character's _friendly_ level. Args: looker (Object): Object doing the looking. """ level = self.friendly_reaction(looker) # looking at the friendly pets makes them nervous... just a little: self.adjust_character(looker, -1) if level == Reaction.SCARED: return self.db.desc + " " + choices(self.db.scared_msg or "It seems scared of you.") elif level == Reaction.CONCERNED: return self.db.desc + " " + choices(self.db.concerned_msg or "It seems concerned you are here.") elif level == Reaction.INTERESTED: return self.db.desc + " " + choices(self.db.interested_msg or "It seems interested in you.") elif level == Reaction.FRIENDLY: return self.db.desc + " " + choices(self.db.friendly_msg or "It seems happy to see you.") else: # If we have an ecstatic message, use it otherwise, grab the friendly: return self.db.desc + " " + choices(self.db.ecstatic_msg or self.db.friendly_msg or "It seems ecstatic to see you.") def update_state(self, *args, **kwargs): """ Hrm. """ super().update_state(*args, **kwargs) self.adjust_all(self.db.loneliness_amount or -1) self.adjust_all_locally(self.db.shyness_amount or 5) # How spammy do we want the pet to be? if random.randint(0, 100) < self.db.active_amount: self.do_action() else: print("Nope") def do_action(self): # Do something based on the highest friendly level is the same area! (level, chars) = self.friendly_reaction() focus = random.choice(chars) if level == Reaction.SCARED: msg = choices(self.db.scared_actions) elif level == Reaction.CONCERNED: msg = choices(self.db.concerned_actions) elif level == Reaction.INTERESTED: msg = choices(self.db.interested_actions) elif level == Reaction.FRIENDLY: msg = choices(self.db.friendly_actions) else: # If we have an ecstatic message, use it otherwise, grab the friendly: msg = choices(self.db.ecstatic_actions or self.db.friendly_actions) focus.msg( sub("", "You", sub("", "you", msg)) ) self.location.msg_contents(sub("<[Yy]ou>", focus.name.title(), msg), exclude=focus)