Add 'pet' command for friendly pets.

This commit is contained in:
Howard Abrams 2025-04-21 21:55:13 -07:00
parent 254a8ace79
commit e6742e4ab8
6 changed files with 182 additions and 26 deletions

View file

@ -4,7 +4,6 @@ from evennia import CmdSet
from evennia.commands.default.general import CmdGive, NumberedTargetCommand
from commands.command import Command
# from typeclasses.pets import Pet
class CmdFeed(Command, NumberedTargetCommand):

24
commands/pets.py Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
from evennia import CmdSet
from commands.command import Command
class CmdPet(Command):
"""
Pet a 'friendly' pet.
"""
key = "pet"
def func(self):
"""
Implements the pet command.
"""
self.obj.pet_response(self.caller)
class CmdPetSet(CmdSet):
"""
Things associated with pets.
"""
def at_cmdset_creation(self):
self.add(CmdPet)

View file

@ -127,10 +127,10 @@ class Character(Object, GenderCharacter):
for thing in self.contents:
to = thing.locks.get('tethered')
if to:
m = match(r".*:id\(#?(.*)\)", to)
m = match(r".*:id\((.*)\)", to)
if m:
id_num = m.group(1)
dest = self.global_search(f"#{id_num}")
dest = self.search(f"{id_num}")
msg = thing.db.tethered_msg
if dest and msg:
thing.location = dest

View file

@ -22,6 +22,7 @@ from typeclasses.objects import Object
from typeclasses.characters import Character
# from typeclasses.lightables import LightSource
from commands.feedables import CmdFeedSet
from commands.pets import CmdPetSet
from utils.word_list import squish, choices, split_party_msg
@ -220,6 +221,8 @@ class Friendly(Pet):
"""
super().at_object_creation()
self.cmdset.add(CmdPetSet)
# We have a list of actions that were spammed to the room:
self.db.last_actions = []
@ -371,7 +374,14 @@ class Friendly(Pet):
def update_state(self, *args, **kwargs):
"""
Hrm.
Call regularly to adjust the pet's reaction state.
Change the state associated with _all_ characters, as well
as characters in the area, based on 'loneliness_amount' and
'shyness_amount' respectively.
Then, if 'active_amount' is triggered, call 'do_action'.
"""
super().update_state(*args, **kwargs)
self.adjust_all(self.db.loneliness_amount or -1)
@ -408,6 +418,33 @@ class Friendly(Pet):
self.db.last_actions = self.db.last_actions[-5:]
split_party_msg(focus, msg)
def pet_response(self, petter):
"""
Called with 'petter' attempts to 'pet' this.
Reaction should be based on petter reaction state.
"""
match self.friendly_reaction(petter):
case Reaction.SCARED:
msg = self.db.pet_scared_response
self.adjust_character(petter, self.db.pet_scared_adjust or 0)
case Reaction.CONCERNED:
msg = self.db.pet_concerned_response
self.adjust_character(petter, self.db.pet_concerned_adjust or 0)
case Reaction.INTERESTED:
msg = self.db.pet_interested_response
self.adjust_character(petter, self.db.pet_interested_adjust or 1)
case Reaction.FRIENDLY:
msg = self.db.pet_friendly_response
self.adjust_character(petter, self.db.pet_friendly_adjust or 8)
case Reaction.ECSTATIC:
msg = self.db.pet_ecstatic_response
self.adjust_character(petter, self.db.pet_ecstatic_adjust or 10)
if msg:
split_party_msg(petter, msg)
else:
petter.msg(f"You pet {self.name}.")
class BHB(Friendly):
def return_appearance(self, looker):
@ -532,7 +569,6 @@ class BHB(Friendly):
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. ;;

View file

@ -49,7 +49,7 @@ class Trinket(Object):
# Once they have seen the crystal ball, they can now "see"
# them, and probably pick one up, if they are around:
if self.db.last_trinket_num == 0:
self.tags.add("hidden_ball")
looker.tags.add("hidden_ball")
# Seen all the trinkets? Oh boy, well, let's loop:
if self.db.last_trinket_num >= len(self.msgs):
@ -99,6 +99,38 @@ class Ring(Object):
return False
class Pipe(Object):
"""Simple abstraction for the following actions.
Note that each message has two versions, one for the smoker (you)
and one for everyone else in the room.
@set pipe/light_msg = "You pull out, pack and light a pipe."
@set pipe/light_msg_other = "{0} packs |p pipe and lights it."
Where the |p is a possessive gender, if set, e.g. his or her.
The random messages has available substitutions based on if the message is for the smoker or for the audience in the room. Specifically:
- {0} :: either "you" or your name
- {1} :: either "your" or your name with an apostrophe 's.
- {2} :: blank (for you) or "s" for everyone else, e.g. "blow{2}"
For instance:
@set pipe/random_msgs = "{0} blow{1} a <<large ^ small ^ >> smoke-ring followed by another that flies through the first. ;; {1} smoke collesce to form a <<dragon ^ large woodland beast ^ beholder ^ bugbear>> ... or
"""
def do_light(self, lighter):
you_msg = choices(self.db.light_msg or "You pack and light your pipe.")
lighter.msg(you_msg)
# desc = self.return_appearance()[:1].lower() + self.return_appearance()[1:]
other_msg = choices(self.db.light_msg_other or "{0} packs and lights |p pipe.", lighter.name)
lighter.location.msg_contents(other_msg, exclude=lighter)
def do_puff(self, smoker):
pass
class Wood(Object):
"An object to burn."
def at_object_creation(self):

View file

@ -1,12 +1,61 @@
#!/usr/bin/env python
import random
import re
from itertools import batched
from random import choice
from re import compile, sub, split
from evennia.utils import logger
def squish(text):
"Remove series of spaces from the text."
return re.sub('[ \n\t]+', ' ', text).strip()
return sub('[ \t]+', ' ', text).strip()
def _routput_choose(text):
"""
Pick a choice when text is: one ^ two ^ three
Done by splitting the text, and calling random.choice().
"""
choices = split(r" *\^ *", text)
return choice(choices)
def _routput_empty(entry):
"""
Return True if entry is empty, e.g. None or blank.
False otherwise. Note: Spaces are ignored.
"""
if entry:
if isinstance(entry, str) and entry.strip() == '':
return True
if isinstance(entry, (list, tuple)) and len(entry) == 0:
return True
return False
return True
def _routput_pair(no_choice, choices = None):
"""
Return a list based on the ^-separated options in 'choices'.
Give a pair of split items, the 'no_choice' is the first section
(before the << ... >>>), and the 'choices' is the section between
the '<< ... >>' delimiter.
"""
# While unlikely, the text before << and the options between the
# << ...>> text may be blank:
if _routput_empty(no_choice) and _routput_empty(choices):
return ''
# If we start a string with <<...>>, then thte 'no_choice' option
# is blank, so we only return a choice:
if _routput_empty(no_choice):
return _routput_choose(choices)
# With text before, but not inside the <<...>> section, we just
# return the first part:
if _routput_empty(choices):
return no_choice
return no_choice + ' ' + _routput_choose(choices)
def routput(text, *substitutions):
"""
@ -22,13 +71,19 @@ def routput(text, *substitutions):
'This feels very comfortable.'
"""
if text:
acc = []
for s in text.split("<<"):
selections, *rest = s.split(">>")
choice = random.choice(re.split(r"\s*\^\s*", selections))
acc = acc + [choice] + rest
# section is a list of some phrase followed by another phrase
# that contains ^-separate choices:
sections = split(r" *<< *(.*?) *>> *", text)
proposal = squish(''.join(acc).format(*substitutions))
# Parts are the first section followed by the _rendered_
# choices, so this^that is either 'this' or 'that':
parts = [_routput_pair(*pair) for pair in batched(sections, 2)]
# The parts may have empty strings, so we filter those out,
# leaving on the phrases to join:
phrases = [phrase for phrase in parts if not _routput_empty(phrase)]
proposal = ' '.join(phrases).format(*substitutions)
# If a choice is at the end of a sentence, and we could have
# "no choice", as in:
@ -36,8 +91,9 @@ def routput(text, *substitutions):
# We don't want a version that looks like:
# "He searches # ."
# with a space before the punctuation:
return re.sub(r"\s+([!?.,])", "\\1", proposal)
#
# Also, we should remove all double spaces that may show:
return squish(sub(r"\s+([!?.,])", "\\1", proposal))
def choices(text, *substitutions):
@ -47,23 +103,32 @@ def choices(text, *substitutions):
Note that text can already be separated as a list or tuple.
"""
if isinstance(text, str):
selections = re.split(r"\s*;;\s*", text)
selections = split(r"\s*;;\s*", text)
elif isinstance(text, (tuple, list)):
selections = text
if selections:
return routput(random.choice(selections), *substitutions)
return routput(choice(selections), *substitutions)
def split_party_msg(viewer, msg):
def split_party_msg(viewer, msg, *substitutions):
"""
Send a message to 'viewer' as well as all people in the area.
Note that 'msg' could have choices separated by ;;
As well as random words, separated by << one ^ two ^ three >>
As well as one word for 'viewer' and other for rest, as in
<( You ^ {0} )> <( give ^ gives )> the ball.
"""
text = choices(msg, viewer.name.title())
pattern = compile(r"\<\( *(.*?) *\^ *(.*?) *\)\>")
# First a message for the view:
viewer.msg(
re.sub("<You>", "You", re.sub("<you>", "you", msg))
)
viewer.msg( pattern.sub("\\1", text) )
# Then the message for the rest of the area:
viewer.location.msg_contents(re.sub("<[Yy]ou>",
viewer.name.title(), msg),
exclude=viewer)
viewer.location.msg_contents(
pattern.sub("\\2", text), exclude=viewer)
# def searsonal(text, **kwargs):
# season = kwargs['season'] or kwargs['location'].get_season()