404 lines
14 KiB
Python
Executable file
404 lines
14 KiB
Python
Executable file
#!/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>", "You", sub("<you>", "you", msg))
|
|
)
|
|
self.location.msg_contents(sub("<[Yy]ou>", focus.name.title(), msg),
|
|
exclude=focus)
|