diff --git a/commands/everyone.py b/commands/everyone.py index 1a8ef42..88490c3 100755 --- a/commands/everyone.py +++ b/commands/everyone.py @@ -123,32 +123,14 @@ class CmdSay(MuxCommand): """ Implements the new 'say' command with switches. """ - def charname(name): - 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}") + speaker = self.caller if not self.args: - self.caller.msg("Say what?") + speaker.msg("Say what?") return 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 characters, use the '=' character to identify what you want to say. @@ -161,7 +143,7 @@ class CmdSay(MuxCommand): speech = self.args # If speech is empty, stop here - if not self.caller.at_pre_say(speech): + if not speaker.at_pre_say(speech): return adverb = '' @@ -188,27 +170,50 @@ class CmdSay(MuxCommand): for_me, for_others, verb = \ speech_effect(speech, verb, - self.caller, - self.caller.attributes.get(category="effect", + speaker, + speaker.attributes.get(category="effect", return_obj=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', # 'You say ...', we have to both send him a message with the # 'You' as well as everyone else with the 'send_emote'. - - to_whom = chars_list(self.lhs, verb, for_rp=False) if 'to' in self.switches else '' - full_speech = f"You {adverb}{verb}{to_whom}, \"{for_me}\"" - self.caller.msg(full_speech, from_obj=self.caller) + full_speech = f"You {adverb}{verb}{to_who}, \"{for_me}\"" + speaker.msg(full_speech, from_obj=speaker) # English is weird... if verb == 'reply': verb = 'replie' - targets = [item for item in self.caller.location.contents if item != self.caller] - to_whom = chars_list(self.lhs, verb) if 'to' in self.switches else '' + targets = [item for item in speaker.location.contents if item != speaker] 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): diff --git a/typeclasses/characters.py b/typeclasses/characters.py index 1aac1b4..19fff03 100644 --- a/typeclasses/characters.py +++ b/typeclasses/characters.py @@ -97,7 +97,9 @@ class Character(Object, GenderCharacter, ContribRPCharacter): This just looks better to me. """ 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 elif match(r"^\|[A-z][a-z].*", line): return line[0:1] + line[2].upper() + line[3:] @@ -326,3 +328,26 @@ class Character(Object, GenderCharacter, ContribRPCharacter): self.spell_sequence(self.location, self.db.reappear_msg.split(';;'), 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 diff --git a/typeclasses/objects.py b/typeclasses/objects.py index 8edbff2..37fd529 100755 --- a/typeclasses/objects.py +++ b/typeclasses/objects.py @@ -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). """ + +from re import split, match + # from evennia.objects.objects import DefaultObject 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 utils.word_list import routput + class ObjectParent: """ @@ -245,6 +250,50 @@ class Object(ObjectParent, ContribRPObject): 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): """ 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) if self.location.is_typeclass("typeclasses.pets.Pet"): 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) diff --git a/typeclasses/puppets.py b/typeclasses/puppets.py index 5be9639..442178e 100755 --- a/typeclasses/puppets.py +++ b/typeclasses/puppets.py @@ -1,5 +1,8 @@ #!/usr/bin/env python +from re import split, match, IGNORECASE +from evennia.utils import logger + from typeclasses.characters import Character from utils.word_list import routput @@ -25,7 +28,7 @@ class Puppet(Character): 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) def at_pre_unpuppet(self, **kwargs): @@ -51,7 +54,7 @@ class Puppet(Character): 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): """ @@ -68,3 +71,62 @@ class Puppet(Character): return self.db.desc_puppeted if self.db.desc_puppeted else self.db.desc else: 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}.\"") diff --git a/utils/word_list.py b/utils/word_list.py index 04c9128..2669fb4 100755 --- a/utils/word_list.py +++ b/utils/word_list.py @@ -115,14 +115,10 @@ def choices(text, *substitutions): Note that text can already be separated as a list or tuple. """ if isinstance(text, str): - selections = split(r"\s*;;\s*", text) - elif isinstance(text, (tuple, list)): - selections = text - else: - selections = None + text = split(r"\s*;;\s*", text) - if selections: - return routput(choice(selections), *substitutions) + if text: + return routput(choice(text), *substitutions) def split_party_msg(viewer, msg, *substitutions):