Add 'pet' command for friendly pets.
This commit is contained in:
parent
254a8ace79
commit
e6742e4ab8
6 changed files with 182 additions and 26 deletions
|
|
@ -4,7 +4,6 @@ from evennia import CmdSet
|
||||||
from evennia.commands.default.general import CmdGive, NumberedTargetCommand
|
from evennia.commands.default.general import CmdGive, NumberedTargetCommand
|
||||||
|
|
||||||
from commands.command import Command
|
from commands.command import Command
|
||||||
# from typeclasses.pets import Pet
|
|
||||||
|
|
||||||
|
|
||||||
class CmdFeed(Command, NumberedTargetCommand):
|
class CmdFeed(Command, NumberedTargetCommand):
|
||||||
|
|
|
||||||
24
commands/pets.py
Executable file
24
commands/pets.py
Executable 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)
|
||||||
|
|
@ -127,10 +127,10 @@ class Character(Object, GenderCharacter):
|
||||||
for thing in self.contents:
|
for thing in self.contents:
|
||||||
to = thing.locks.get('tethered')
|
to = thing.locks.get('tethered')
|
||||||
if to:
|
if to:
|
||||||
m = match(r".*:id\(#?(.*)\)", to)
|
m = match(r".*:id\((.*)\)", to)
|
||||||
if m:
|
if m:
|
||||||
id_num = m.group(1)
|
id_num = m.group(1)
|
||||||
dest = self.global_search(f"#{id_num}")
|
dest = self.search(f"{id_num}")
|
||||||
msg = thing.db.tethered_msg
|
msg = thing.db.tethered_msg
|
||||||
if dest and msg:
|
if dest and msg:
|
||||||
thing.location = dest
|
thing.location = dest
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from typeclasses.objects import Object
|
||||||
from typeclasses.characters import Character
|
from typeclasses.characters import Character
|
||||||
# from typeclasses.lightables import LightSource
|
# from typeclasses.lightables import LightSource
|
||||||
from commands.feedables import CmdFeedSet
|
from commands.feedables import CmdFeedSet
|
||||||
|
from commands.pets import CmdPetSet
|
||||||
from utils.word_list import squish, choices, split_party_msg
|
from utils.word_list import squish, choices, split_party_msg
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -220,6 +221,8 @@ class Friendly(Pet):
|
||||||
"""
|
"""
|
||||||
super().at_object_creation()
|
super().at_object_creation()
|
||||||
|
|
||||||
|
self.cmdset.add(CmdPetSet)
|
||||||
|
|
||||||
# We have a list of actions that were spammed to the room:
|
# We have a list of actions that were spammed to the room:
|
||||||
self.db.last_actions = []
|
self.db.last_actions = []
|
||||||
|
|
||||||
|
|
@ -371,7 +374,14 @@ class Friendly(Pet):
|
||||||
|
|
||||||
def update_state(self, *args, **kwargs):
|
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)
|
super().update_state(*args, **kwargs)
|
||||||
self.adjust_all(self.db.loneliness_amount or -1)
|
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:]
|
self.db.last_actions = self.db.last_actions[-5:]
|
||||||
split_party_msg(focus, msg)
|
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):
|
class BHB(Friendly):
|
||||||
def return_appearance(self, looker):
|
def return_appearance(self, looker):
|
||||||
|
|
@ -532,7 +569,6 @@ class BHB(Friendly):
|
||||||
case Reaction.CONCERNED:
|
case Reaction.CONCERNED:
|
||||||
msg = "The beast walks over to the stick, sniffs it, and backs away."
|
msg = "The beast walks over to the stick, sniffs it, and backs away."
|
||||||
self.adjust_character(thrower, 5)
|
self.adjust_character(thrower, 5)
|
||||||
|
|
||||||
case Reaction.INTERESTED:
|
case Reaction.INTERESTED:
|
||||||
msg = choices("""
|
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 <<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. ;;
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class Trinket(Object):
|
||||||
# Once they have seen the crystal ball, they can now "see"
|
# Once they have seen the crystal ball, they can now "see"
|
||||||
# them, and probably pick one up, if they are around:
|
# them, and probably pick one up, if they are around:
|
||||||
if self.db.last_trinket_num == 0:
|
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:
|
# Seen all the trinkets? Oh boy, well, let's loop:
|
||||||
if self.db.last_trinket_num >= len(self.msgs):
|
if self.db.last_trinket_num >= len(self.msgs):
|
||||||
|
|
@ -99,6 +99,38 @@ class Ring(Object):
|
||||||
return False
|
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):
|
class Wood(Object):
|
||||||
"An object to burn."
|
"An object to burn."
|
||||||
def at_object_creation(self):
|
def at_object_creation(self):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,61 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import random
|
from itertools import batched
|
||||||
import re
|
from random import choice
|
||||||
|
from re import compile, sub, split
|
||||||
|
|
||||||
|
from evennia.utils import logger
|
||||||
|
|
||||||
def squish(text):
|
def squish(text):
|
||||||
"Remove series of spaces from the 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):
|
def routput(text, *substitutions):
|
||||||
"""
|
"""
|
||||||
|
|
@ -22,13 +71,19 @@ def routput(text, *substitutions):
|
||||||
'This feels very comfortable.'
|
'This feels very comfortable.'
|
||||||
"""
|
"""
|
||||||
if text:
|
if text:
|
||||||
acc = []
|
# section is a list of some phrase followed by another phrase
|
||||||
for s in text.split("<<"):
|
# that contains ^-separate choices:
|
||||||
selections, *rest = s.split(">>")
|
sections = split(r" *<< *(.*?) *>> *", text)
|
||||||
choice = random.choice(re.split(r"\s*\^\s*", selections))
|
|
||||||
acc = acc + [choice] + rest
|
|
||||||
|
|
||||||
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
|
# If a choice is at the end of a sentence, and we could have
|
||||||
# "no choice", as in:
|
# "no choice", as in:
|
||||||
|
|
@ -36,8 +91,9 @@ def routput(text, *substitutions):
|
||||||
# We don't want a version that looks like:
|
# We don't want a version that looks like:
|
||||||
# "He searches # ."
|
# "He searches # ."
|
||||||
# with a space before the punctuation:
|
# 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):
|
def choices(text, *substitutions):
|
||||||
|
|
@ -47,23 +103,32 @@ def choices(text, *substitutions):
|
||||||
Note that text can already be separated as a list or tuple.
|
Note that text can already be separated as a list or tuple.
|
||||||
"""
|
"""
|
||||||
if isinstance(text, str):
|
if isinstance(text, str):
|
||||||
selections = re.split(r"\s*;;\s*", text)
|
selections = split(r"\s*;;\s*", text)
|
||||||
elif isinstance(text, (tuple, list)):
|
elif isinstance(text, (tuple, list)):
|
||||||
selections = text
|
selections = text
|
||||||
|
|
||||||
if selections:
|
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:
|
# First a message for the view:
|
||||||
viewer.msg(
|
viewer.msg( pattern.sub("\\1", text) )
|
||||||
re.sub("<You>", "You", re.sub("<you>", "you", msg))
|
|
||||||
)
|
|
||||||
# Then the message for the rest of the area:
|
# Then the message for the rest of the area:
|
||||||
viewer.location.msg_contents(re.sub("<[Yy]ou>",
|
viewer.location.msg_contents(
|
||||||
viewer.name.title(), msg),
|
pattern.sub("\\2", text), exclude=viewer)
|
||||||
exclude=viewer)
|
|
||||||
|
|
||||||
# def searsonal(text, **kwargs):
|
# def searsonal(text, **kwargs):
|
||||||
# season = kwargs['season'] or kwargs['location'].get_season()
|
# season = kwargs['season'] or kwargs['location'].get_season()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue