Automated puppets react on triggers

This commit is contained in:
Howard Abrams 2025-05-25 22:06:52 -07:00
parent 428fa8334b
commit 2e69558b3f
5 changed files with 187 additions and 42 deletions

View file

@ -123,32 +123,14 @@ class CmdSay(MuxCommand):
""" """
Implements the new 'say' command with switches. Implements the new 'say' command with switches.
""" """
def charname(name): speaker = self.caller
try:
results = self.caller.search(name, quiet=True)
return results.get_display_name(self)
except:
return name
def chars_for_self(chars):
return [charname(c) for c in split(r"[ ,]+", chars)]
def chars_for_others(chars):
return [c if c.startswith('/') else '/'+c for c in split(r"[ ,]+", chars)]
def chars_list(chars, verb, for_rp=True):
char_lst = chars_for_others(chars) if for_rp else chars_for_self(chars)
return (' to ' if verb == 'say' else ' ') + \
iter_to_str(char_lst, endsep='and')
# logger.info(f"CmdSayIt: {self.cmdstring} lhs={self.lhs} switches={self.switches}")
if not self.args: if not self.args:
self.caller.msg("Say what?") speaker.msg("Say what?")
return return
if 'to' in self.switches and not self.rhs: if 'to' in self.switches and not self.rhs:
self.caller.msg(paragraph(""" speaker.msg(paragraph("""
When attempting to say something to one or more When attempting to say something to one or more
characters, use the '=' character to identify what you characters, use the '=' character to identify what you
want to say. want to say.
@ -161,7 +143,7 @@ class CmdSay(MuxCommand):
speech = self.args speech = self.args
# If speech is empty, stop here # If speech is empty, stop here
if not self.caller.at_pre_say(speech): if not speaker.at_pre_say(speech):
return return
adverb = '' adverb = ''
@ -188,27 +170,50 @@ class CmdSay(MuxCommand):
for_me, for_others, verb = \ for_me, for_others, verb = \
speech_effect(speech, verb, speech_effect(speech, verb,
self.caller, speaker,
self.caller.attributes.get(category="effect", speaker.attributes.get(category="effect",
return_obj=True, return_obj=True,
return_list=True)) return_list=True))
to_who = to_whom = ''
if 'to' not in self.switches:
for char in speaker.location.contents:
if hasattr(char, 'other_say') and callable(char.other_say):
char.other_say(speaker, for_others)
else:
# Send the message to 'puppets' that have the special 'other_sayto'
# method (if they are the object of the message):
targets = split(r" *, *", self.lhs)
to_chars = [speaker.search(c, quiet=True)[0] for c in targets]
for char in to_chars:
if hasattr(char, 'other_sayto') and callable(char.other_sayto):
logger.info(f"Found {char.key}: {for_others}")
char.other_sayto(speaker, for_others)
who = iter_to_str([c.get_display_name(speaker) for c in to_chars])
whom = iter_to_str([f"/{c}" for c in targets])
if verb == "ask":
to_who = " " + who
to_whom = " " + whom
else:
to_who = " to " + who
to_whom = " to " + whom
# The `send_emote` is _global_, so if we want to say to 'Bob', # The `send_emote` is _global_, so if we want to say to 'Bob',
# 'You say ...', we have to both send him a message with the # 'You say ...', we have to both send him a message with the
# 'You' as well as everyone else with the 'send_emote'. # 'You' as well as everyone else with the 'send_emote'.
full_speech = f"You {adverb}{verb}{to_who}, \"{for_me}\""
to_whom = chars_list(self.lhs, verb, for_rp=False) if 'to' in self.switches else '' speaker.msg(full_speech, from_obj=speaker)
full_speech = f"You {adverb}{verb}{to_whom}, \"{for_me}\""
self.caller.msg(full_speech, from_obj=self.caller)
# English is weird... # English is weird...
if verb == 'reply': if verb == 'reply':
verb = 'replie' verb = 'replie'
targets = [item for item in self.caller.location.contents if item != self.caller] targets = [item for item in speaker.location.contents if item != speaker]
to_whom = chars_list(self.lhs, verb) if 'to' in self.switches else ''
full_speech = f"/me {adverb}{verb}s{to_whom}, \"{for_others}\"" full_speech = f"/me {adverb}{verb}s{to_whom}, \"{for_others}\""
send_emote(self.caller, targets, full_speech, msg_type="say", anonymous_add=None) send_emote(speaker, targets, full_speech, msg_type="say", anonymous_add=None)
class CmdThink(Command): class CmdThink(Command):

View file

@ -97,7 +97,9 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
This just looks better to me. This just looks better to me.
""" """
def capitalize_line(line): def capitalize_line(line):
if match(r"^\|[bm][a-z].*", line): if not line or line.strip() == "":
return line
elif match(r"^\|[bm][a-z].*", line):
return "The " + line return "The " + line
elif match(r"^\|[A-z][a-z].*", line): elif match(r"^\|[A-z][a-z].*", line):
return line[0:1] + line[2].upper() + line[3:] return line[0:1] + line[2].upper() + line[3:]
@ -326,3 +328,26 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
self.spell_sequence(self.location, self.spell_sequence(self.location,
self.db.reappear_msg.split(';;'), self.db.reappear_msg.split(';;'),
self.db.appear_delay or 2) self.db.appear_delay or 2)
# Hooks to the puppets:
def puppets_here(self):
"""
Return a list of puppets in the current location.
Only used for calling hooks on the animatronic dolls.
"""
return [puppet for puppet in
self.search("", typeclass="typeclasses.puppets.Puppet",
location=self.location, quiet=True)
if puppet != self]
def at_post_move(self, past_location, move_type="move", **kwargs):
super().at_post_move(past_location, move_type)
for puppet in self.puppets_here():
puppet.other_arrive(self)
def at_pre_move(self, destination, move_type="move", **kwargs):
super().at_pre_move(destination, move_type)
for puppet in self.puppets_here():
puppet.other_leave(self)
return True

View file

@ -8,11 +8,16 @@ Use the ObjectParent class to implement common features for *all* entities
with a location in the game world (like Characters, Rooms, Exits). with a location in the game world (like Characters, Rooms, Exits).
""" """
from re import split, match
# from evennia.objects.objects import DefaultObject # from evennia.objects.objects import DefaultObject
from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject
from evennia.utils import delay from evennia.utils import delay, logger
# from evennia.utils import delay, logger, search # from evennia.utils import delay, logger, search
from utils.word_list import routput
class ObjectParent: class ObjectParent:
""" """
@ -245,6 +250,50 @@ class Object(ObjectParent, ContribRPObject):
return None return None
def delay_sequence(self, sequence_str, time_delay=1, *args):
"""Run a sequence of messages or commands with a delay.
The 'sequence_str' is a number of commands separated by ';;'
character sequence. The command can by standard things like
'say' or 'give', but can be a message delivered to the room
location, if it begins with a # or 'gm'.
The command can also be a number, in which case, the next
command (and all subsequent commands), will be delayed by that
number of seconds.
For instance:
'say Hello there, want a drink? ;; 8 ;; # He works on shaking a cocktail. ;; 2 ;; shake whisky = avatar'
Which could show:
Blonde elf says, "Hello there, want a drink?"
<8 seconds pass>
He works on shaking a cocktail.
<2 seconds pass>
You now have a whisky.
"""
def convert(x):
try:
return int(x)
except ValueError:
return x
lines = [convert(line) for line in split(r" *;; *", sequence_str)]
pause = 0
for line in lines:
if isinstance(line, int):
time_delay = line
else:
pause = pause + time_delay
m = match(r"(# *|gm +)(.*)", line)
if m:
delay(pause, self.location.msg_contents, routput(m.group(2), *args))
else:
delay(pause, self.execute_cmd, routput(line, *args))
def at_post_move(self, source_location, move_type="move", **kwargs): def at_post_move(self, source_location, move_type="move", **kwargs):
""" """
Delete ourselves if we are living inside a pet. Delete ourselves if we are living inside a pet.
@ -252,3 +301,11 @@ class Object(ObjectParent, ContribRPObject):
super().at_post_move(source_location, move_type) super().at_post_move(source_location, move_type)
if self.location.is_typeclass("typeclasses.pets.Pet"): if self.location.is_typeclass("typeclasses.pets.Pet"):
delay(5, self.delete) delay(5, self.delete)
def at_give(self, giver, getter, **kwargs):
"""
Call a puppet's 'other_given' method, if defined.
"""
super().at_give(giver, getter)
if hasattr(getter, 'other_given') and callable(getter.other_given):
getter.other_given(giver, self)

View file

@ -1,5 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
from re import split, match, IGNORECASE
from evennia.utils import logger
from typeclasses.characters import Character from typeclasses.characters import Character
from utils.word_list import routput from utils.word_list import routput
@ -25,7 +28,7 @@ class Puppet(Character):
puppeting this Object. puppeting this Object.
""" """
self.msg("\nYou are puppeting |c{name}|n.".format(name=self.key)) self.msg(f"\nYou are puppeting |c{self.key}|n.")
self.msg((self.at_look(self.location), {"type": "look"}), options=None) self.msg((self.at_look(self.location), {"type": "look"}), options=None)
def at_pre_unpuppet(self, **kwargs): def at_pre_unpuppet(self, **kwargs):
@ -51,7 +54,7 @@ class Puppet(Character):
overriding the call (unused by default). overriding the call (unused by default).
""" """
self.msg("\nNo longer puppeting |c{name}|n.\n".format(name=self.key)) self.msg(f"\nNo longer puppeting |c{self.key}|n.\n")
def get_display_desc(self, _, **kwargs): def get_display_desc(self, _, **kwargs):
""" """
@ -68,3 +71,62 @@ class Puppet(Character):
return self.db.desc_puppeted if self.db.desc_puppeted else self.db.desc return self.db.desc_puppeted if self.db.desc_puppeted else self.db.desc
else: else:
return self.db.desc_unpuppeted if self.db.desc_unpuppeted else self.db.desc return self.db.desc_unpuppeted if self.db.desc_unpuppeted else self.db.desc
# ----------------------------------------------------------------------
# TRIGGERS
def get_character_label(self, character):
return character.get_display_name(self).split(' ')[-1]
def trigger_sequence(self, seq, character, *other_stuff):
self.delay_sequence(seq, 1, character.key, character.get_display_name(self),
self.get_character_label(character), *other_stuff)
def get_attribute_trigger(self, label, character):
"""
Return the attribute where 'key' is the label, and the
category can by either the 'character' or its display name.
"""
name = self.get_character_label(character)
return (self.attributes.get(key=label, category=character.key) or
self.attributes.get(key=label, category=name) or
self.attributes.get(key=label))
def move_triggers(self, label, character):
"""
Return a list of triggers matching 'label' and 'character'.
"""
seq = self.get_attribute_trigger(label, character)
if seq:
self.trigger_sequence(seq, character)
def other_arrive(self, character):
"""
Execute a command when a character arrives in the same location.
"""
self.move_triggers('arrive', character)
def other_leave(self, character):
"""
Execute a command when a character arrives in the same location.
"""
self.move_triggers('leave', character)
def say_triggers(self, label, character, speech):
trigs = self.get_attribute_trigger(label, character)
if trigs:
for _, trigger in enumerate(dict(trigs)):
seq = trigs[trigger]
if match(trigger, speech, IGNORECASE):
self.trigger_sequence(seq, character)
def other_say(self, speaker, speech):
self.say_triggers('say', speaker, speech)
def other_sayto(self, speaker, speech):
self.say_triggers('sayto', speaker, speech)
def other_given(self, giver, obj):
target = giver.get_display_name(self).split(' ')[-1]
self.execute_cmd(f"emote /Me says to /{target}, \"Thanks for the {obj.name}.\"")

View file

@ -115,14 +115,10 @@ 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 = split(r"\s*;;\s*", text) text = split(r"\s*;;\s*", text)
elif isinstance(text, (tuple, list)):
selections = text
else:
selections = None
if selections: if text:
return routput(choice(selections), *substitutions) return routput(choice(text), *substitutions)
def split_party_msg(viewer, msg, *substitutions): def split_party_msg(viewer, msg, *substitutions):