551 lines
20 KiB
Python
Executable file
551 lines
20 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 evennia.utils.gametime import schedule
|
|
|
|
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, split_party_msg
|
|
|
|
|
|
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
|
|
|
|
# Update the state (whatever that may mean), each minute:
|
|
TICKER_HANDLER.add(interval=60, 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
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Migrating Pets move from room to room during particular times
|
|
|
|
# ----------------------------------------------------------------------
|
|
# 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()
|
|
|
|
# We have a list of actions that were spammed to the room:
|
|
self.db.last_actions = []
|
|
|
|
# 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 or 3):
|
|
self.do_action()
|
|
|
|
def do_action(self):
|
|
# Do something based on the highest friendly level is the same
|
|
# area!
|
|
msg = None
|
|
(level, chars) = self.friendly_reaction()
|
|
if chars and len(chars) > 0:
|
|
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)
|
|
|
|
if msg and msg not in self.db.last_actions:
|
|
self.db.last_actions.append(msg)
|
|
# Limit this list so it doesn't grow too large:
|
|
self.db.last_actions = self.db.last_actions[-5:]
|
|
split_party_msg(focus, msg)
|
|
|
|
|
|
class BHB(Friendly):
|
|
def return_appearance(self, looker):
|
|
if self.db.is_awake:
|
|
return super().return_appearance(looker)
|
|
else:
|
|
return "It currently slumbers on its huge mattress."
|
|
|
|
def update_state(self, *args, **kwargs):
|
|
msg = None
|
|
wake_hour = self.db.wake_hour or 8
|
|
sleep_hour = self.db.sleep_hour or 20
|
|
|
|
(minute, hour, tod, season) = self.location.get_time()
|
|
|
|
if hour >= wake_hour and hour < sleep_hour:
|
|
self.db.is_awake = True
|
|
else:
|
|
self.db.is_awake = False
|
|
|
|
noun = random.choice(["beast", "BHB", "monstrous beast",
|
|
"big, hairy beast"])
|
|
|
|
if hour == wake_hour - 1:
|
|
if minute == 53:
|
|
msg = f"The {noun} begins to stir from slumber."
|
|
elif minute == 56:
|
|
msg = f"The {noun} rises and stretches ..."
|
|
elif minute == 58:
|
|
msg = f"The {noun} lets loose a big yawn."
|
|
|
|
if hour == sleep_hour - 1:
|
|
if minute == 53:
|
|
msg = f"The {noun} look up at the darkening sky."
|
|
elif minute == 56:
|
|
msg = f"The {noun} blinks. It looks tired."
|
|
elif minute == 58:
|
|
msg = f"The {noun} lets loose a big yawn."
|
|
|
|
meadow = self.global_search("mp05")
|
|
cave = self.global_search("mp07")
|
|
if self.db.is_awake and self.location != meadow:
|
|
self.move_to(meadow, move_type="traverse")
|
|
elif not self.db.is_awake and self.location != cave:
|
|
self.move_to(cave, move_type="traverse")
|
|
|
|
if msg and self.location:
|
|
self.location.msg_contents(msg)
|
|
else:
|
|
# This might call the do_action():
|
|
super().update_state(*args, **kwargs)
|
|
|
|
def do_action(self):
|
|
"""
|
|
We only override the Friendly 'do_action' if the beast is
|
|
asleep on its huge pilla'.
|
|
"""
|
|
if self.db.is_awake:
|
|
super().do_action()
|
|
else:
|
|
msg = choices(self.db.sleeping_actions)
|
|
if msg != self.db.last_actions:
|
|
self.db.last_actions.append(msg)
|
|
# Limit this list so it doesn't grow too large:
|
|
self.db.last_actions = self.db.last_actions[-5:]
|
|
|
|
self.location.msg_contents(msg)
|
|
|
|
def feed(self, feeder, item=None):
|
|
"""
|
|
Feeding the beast. If item is None, we choose something
|
|
the character has, and go with that...
|
|
"""
|
|
# Categorize items that can be used to feed the beast:
|
|
def is_berry(item):
|
|
return (not item and feeder.has("berries")) or (item and item.key == 'berries')
|
|
def is_scone(item):
|
|
return (not item and feeder.has("scone")) or (item and item.key == 'scone')
|
|
|
|
# Based on the reaction to the feeder, the adjectives may alter:
|
|
noun = "The " + random.choice(["huge", "big, hairy"]) + " beast"
|
|
|
|
match self.friendly_reaction(feeder):
|
|
case Reaction.SCARED:
|
|
how_sniff = "carefully"
|
|
how_eat = "cautiously"
|
|
and_then = "an then runs away to the edge of the meadow"
|
|
case Reaction.CONCERNED:
|
|
how_sniff = "cautiously"
|
|
how_eat = "gingerly"
|
|
and_then = "slowly backs away to a safe distance"
|
|
case Reaction.INTERESTED:
|
|
how_sniff = "curiously"
|
|
how_eat = "quickly"
|
|
and_then = "waits to see what else you might have"
|
|
case _:
|
|
how_sniff = "eagerly"
|
|
how_eat = "excitedly"
|
|
and_then = "gives you a big lick as if to say, Thank you"
|
|
|
|
if is_scone(item):
|
|
msg = f"{noun} {how_sniff} sniffs your outstretched arm holding the scone. It {how_eat} eats it, and {and_then}."
|
|
self.adjust_character(feeder, 100)
|
|
feeder.has('scone').delete()
|
|
elif is_berry(item):
|
|
msg = f"{noun} {how_sniff} sniffs your outstretched arm with a handful of berries. It {how_eat} eats them, and {and_then}."
|
|
self.adjust_character(feeder, 40)
|
|
feeder.has('berries').delete()
|
|
else:
|
|
msg = f"{noun} doesn't appear interesting in anything you have."
|
|
|
|
feeder.msg(msg)
|
|
|
|
def thrown_stick(self, thrower):
|
|
"""
|
|
Called when thrower is in the vicinity of the beast, and
|
|
throws a stick they may have.
|
|
"""
|
|
match self.friendly_reaction(thrower):
|
|
case Reaction.SCARED:
|
|
msg = "The beast runs away when you throw the stick."
|
|
case Reaction.CONCERNED:
|
|
msg = "The beast walks over to the stick, sniffs it, and backs away."
|
|
self.adjust_character(thrower, 5)
|
|
|
|
case Reaction.INTERESTED:
|
|
msg = choices("""
|
|
The beast {{runs ^ hurries}} at the stick, then looks at <you>, {{wondering ^ curious as to ^ pondering}} what to do with a stick the flies back to its owner. ;;
|
|
The beast {{rushes ^ runs at}} the stick, and then veers off, thundering around the {{field ^ meadow}}. The stick returns to your hand.
|
|
""")
|
|
self.adjust_character(thrower, 30)
|
|
|
|
case _:
|
|
msg = choices("""
|
|
The {{big hairy ^ }} beast {{dashes ^ runs ^ jumps ^ leaps into the air ^ leaps}} and {{catches ^ snags ^ grabs}} the stick in midair, and drops it in front of <you>. ;;
|
|
The {{big hairy ^ }} beast {{gallops ^ trumbles ^ trots}} over to the stick and {{dances ^ prances ^ hops}} around the stick before bringing it back to <you>. ;;
|
|
The {{big hairy ^ }} beast {{gallops ^ trumbles ^ trots}} over to the stick and {{dances ^ prances ^ hops}} around the stick before bringing it back to <you>.""")
|
|
self.adjust_character(thrower, 10)
|
|
if msg:
|
|
split_party_msg(thrower, msg)
|