diff --git a/commands/default_cmdsets.py b/commands/default_cmdsets.py index f17f3be..457529d 100644 --- a/commands/default_cmdsets.py +++ b/commands/default_cmdsets.py @@ -17,6 +17,7 @@ own cmdsets by inheriting from them or directly from `evennia.CmdSet`. from evennia import default_cmds from evennia.contrib.grid import extended_room from commands.sittables import CmdNoSitStand +from commands.take import CmdTake class CharacterCmdSet(default_cmds.CharacterCmdSet): """ @@ -33,6 +34,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): """ super().at_cmdset_creation() self.add(CmdNoSitStand) + self.add(CmdTake()) self.add(extended_room.ExtendedRoomCmdSet) # # any commands you add below will overload the default ones. diff --git a/commands/take.py b/commands/take.py new file mode 100755 index 0000000..c28589a --- /dev/null +++ b/commands/take.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +from commands.command import Command +from evennia import CmdSet +from evennia.utils import logger + +class CmdTake(Command): + """ + Steals an object from another. + Added to the default_cmdsets. + """ + key = "take" + aliases = ["get", "remove"] + # only allow this command if command.obj is carried by caller. + # locks = "cmd:holds()" + + def func(self): + """ + Implements the take command. Since this command is designed + to work on the object, we operate only on self.obj. + """ + logger.log_info(f"Dealing with {self.caller.key}") + self.caller.do_take(self.args) diff --git a/typeclasses/characters.py b/typeclasses/characters.py index 9b0e200..bc9a461 100644 --- a/typeclasses/characters.py +++ b/typeclasses/characters.py @@ -10,6 +10,7 @@ creation commands. from evennia.objects.objects import DefaultCharacter +from utils.word_list import Token, routput from .objects import ObjectParent @@ -22,6 +23,32 @@ class Character(ObjectParent, DefaultCharacter): properties and methods available on all Object child classes like this. """ + def do_take(self, args): + """ + A character has a _steal_command. What are the limitations? + """ + args = Token(args) + if args.empty(): + self.msg("What do you want to take?") + elif len(args.words) == 1: + self.msg(f"You want to take {args.words[0]}, but from whom?") + else: + to_take = args.words[0] + from_whom = args.words[-1] + victim = self.search(from_whom) + if victim: + thing = victim.has(to_take) + if thing: + self.msg(f"You take {thing.key} from {victim.key}.") + self.location.msg_contents( + f"{self.key} takes {thing.key} from {victim.key}!", + exclude=self) + thing.move_to(self, quiet=True, use_destination=True) + return + + self.msg(f"{victim.key} doesn't have a {to_take}.") + # else: + # self.msg(f"You don't see a {from_whom}.") def at_pre_move(self, destination, **kwargs): """ diff --git a/typeclasses/drinkables.py b/typeclasses/drinkables.py index 8f25559..3203895 100755 --- a/typeclasses/drinkables.py +++ b/typeclasses/drinkables.py @@ -2,7 +2,7 @@ from typeclasses.objects import Object from commands.drinkables import CmdSetTeapot, CmdSetTeacup -from utils.word_list import routput, character_has, Token +from utils.word_list import routput, Token import random @@ -110,6 +110,6 @@ class Teapot(Object): self.location.msg_contents(f"{drinker.name} makes a pot of tea.", exclude=drinker) def do_fill(self, drinker): - teacup = character_has(drinker, "teacup") + teacup = drinker.has("teacup") if teacup: teacup.do_fill(drinker, self.db.tea_type, TEA_TYPES[self.db.tea_type]) diff --git a/typeclasses/objects.py b/typeclasses/objects.py index 1cff532..662276c 100644 --- a/typeclasses/objects.py +++ b/typeclasses/objects.py @@ -215,8 +215,21 @@ class Object(ObjectParent, DefaultObject): at_desc(looker=None) """ + def has(self, item): + """ + Return true if object has an item. + Where item is probably a string name to match an item's key. + It can also be a type, for instance: - pass + character.has(typeclasses.drinkables.TeaCup) + """ + for i in self.contents: + if isinstance(item, str) and (i.key == item or i.aliases.get(item)): + return i + elif type(i) == item: + return i + elif i == item: + return i # ------------------------------------------------------------- # diff --git a/typeclasses/rooms.py b/typeclasses/rooms.py index ab4d39e..a386f22 100644 --- a/typeclasses/rooms.py +++ b/typeclasses/rooms.py @@ -13,7 +13,7 @@ from .objects import LightSource from .drinkables import TEACUP_DESCS from .pets import Hunger from commands.drinkables import CmdSetTrolley -from utils.word_list import routput, character_has, Token +from utils.word_list import routput, Token import random @@ -305,7 +305,7 @@ class DabblersRoom(Room): self.cmdset.add_default(CmdSetTrolley, persistent=True) def produce_teacup(self, caller): - if character_has(caller, "teacup"): + if caller.has("teacup"): caller.msg("You already have a teacup.") else: cuptype = self.teacup_prototype diff --git a/typeclasses/things.py b/typeclasses/things.py new file mode 100755 index 0000000..de93515 --- /dev/null +++ b/typeclasses/things.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python + +""" +Knocker + +The Object is the class for gatekeepers between rooms. + +Use the ObjectParent class to implement common features for *all* entities +with a location in the game world (like Characters, Rooms, Exits). + +""" + +from evennia.utils import logger + +from .objects import Object +from .characters import Character +from utils.word_list import routput + +from random import choice, random +import re + + +class Returnable(Object): + """ + This object can't go far from one or two locations. + + """ + pass + +class Knocker(Object): + """ + Special object that listens to what is said in the room, and + attempts to a _real_ NPC. + + """ + muffled_responses = [ + "Mmmuufffmm", + "Mmmf?", + "Mummffmmph", + "Umfmuummmfff", + "Umf! Umfmmmf mmm mmmuufffmm mff mmuuummph.", + ] + + # Note that the responses are ordered from most specific to least. + all_responses = [ + # The password must be the first entry here, as we "act" on it: + ["whiske?y", [ + "Alright, I'll open the door for yah.", + "I heard my favorite, let's get this door unlocked.", + ]], + [r"knocker|goblin", [ + "[Sorry.|What was that?|Did you say something?] I'm hard of hearing on account of the brass ears.", + "Yes, I suppose I'm this amazing puzzle you get to in Chapter Three. Wait, does that mean I'm just an NPC?", + "Who me? I thought you were talking to the goblin in the bushes." + "Why yes, I am hard of hearing.", + ]], + ["password", [ + "Of course this door is protected by a super complicated encrypted password.", + "If I tell you, it wouldn't be a secret now.", + "The password? You just have to guess.", + "Well, I suppose I could give you a hint." + ]], + ["hint", [ + "A hint does sound fair. [Should I|I should] come up with a |briddle|n[|, huh]?" + ]], + ["riddle", [ + "Aged in barrels, smooth and neat, in a glass by the fire, I'm a treat.", + "A scent of oak, a whiff of grain, a drop of expertise from the cask remains.", + """ + In oaken halls, I sleep and bide, + till I'm called to warm your insides... + hrm, that's a little vague, but kinda nice. + """, + """ + Alright, alright, the riddle should be clever. + It should refer to its golden hue, and I should make it obvious + that it isn't gold, and adding a reference to quest of the Argonauts + would be a complete red herring, and quite a mean thing to do, + so I shouldn't add that, + but what about barrels? Yeah, need to include barrels, but not the + way Bilbo road barrels, for that was definitely intended as a misleading + riddle for Smaug, but of course, a smart dragon would always figure such + things out, wait, where was I? + """ + ]], + ["hello|greet", [ + "How's it going?", + "How's it?", + "'Sup.", + "How are you?", + ]], + [r"\bass\b", [ + "Why yes, I am made of brass." + ]], + [r"\block", [ + "Yes, I'm familiar with the door and the fact that it is locked.", + "This locked door is to protect the theft of Dabbler's scones.", + ]], + [r"\bdoor\b", [ + "What door? I don't see a door. Ha!", + "That's right, I am the clever puzzle hanging on a door.", + "I'm hanging on a door? Really? Let's see, can I roll my eyes?", + "Just to let you know, the door is locked.", + """ + I shouldn['t|be quiet, and not] be telling you this, + but I like the cut of your [suit|cloak|jib]. + So, you see, if you speak the |bpassword|n, wait, I've said too much. + """ + ]], + ["\byes|yeah|yah\b", [ + "Really? You agree?", + "Excellent", + ]], + ["\bno|nope\b", [ + "Well, it's true. Just ask the [raven|trees|gnome].", + ]], + ["\?$", [ + "What about [me|it|'im]?", + "I dunno...", + ]], + ] + + after_unlocked_responses = [ + """ + We could [feign|pretend|play make-believe] and + [carry on|continue|persist] this [conversation|charade], + but we both know that |byou|n know the [password|secret|secret word|magic], + and can go through the |bdoor|n now. + """, + """ + [Sure|Why not|Why yes], let's [feign|pretend|play make-believe that] you + don't know the [password|secret|secret word|magic], and + [carry on|continue|persist] this conversation. + """, + ] + + cant_hear_responses = [ + "[Sorry.|What was that?|Did you say something?] I'm hard of hearing on account of the brass ears.", + "Brass ears. Yeah, not the best at hearing.", + "Yeah, These brass ears don't hear much except for the |b[secret|magic|] password|n.", + ] + + unknown_responses = [ + "Are you talking to me or the goblin [in the bushes|up the tree|behind the rock]?", + "Knock, knock.", + "What do you mean?", + "Of course I like [hard candy|squirrels|apples]. Who [doesn't|wouldn't|do you know that doesn't]?", + "No thank you, I can't eat [apples|kumquats|figs], what with the brass teeth and a lack of guts.", + "Tea? While that would be [nice|sweet] of you, I can't really hold a cup.", + "I'm fine, thanks. How are you?", + "I'll say, [we have had|that is] a spell of weather.", + ] + + def at_heard_say(self, message, from_obj): + """ + A simple listener and response. This makes it easy to change for + subclasses of NPCs reacting differently to says. + + """ + # message will be on the form ` says, "say_text"` + # we want to get only say_text without the quotes and any spaces + message = message.split('says, ')[1].strip(' "') + + # Let's see if a keyword gives a good response: + for idx, [regex, responses] in enumerate(self.all_responses): + if re.match(r".*" + regex + r".*", message): + # The first match is the password, so we set a tag on + # the character, so that they can go through the door: + if idx == 0: + from_obj.tags.add("open_red_door", category="mp") + + # If we have the ring in our mouth, we are muffled: + if self.has("brass ring"): + return choice(self.muffled_responses) + else: + return routput(choice(responses)) + + # We can not do a random response. Of course, we are + # pretending we are hard of hearing, so we don't spam the room + # _every_ time: + if random() * 100 < 45: + if self.has("brass ring"): + return choice(self.muffled_responses) + + # If a keyword was not spoken, we want to emphasize that + # we are hard of hearing, and mention that every other + # time, so we store in the database a setting to keep + # track and alternate the responses: + elif self.db.hard: + self.db.hard = False; + if from_obj.tags.get(key="open_red_door", category="mp"): + return routput(choice(self.after_unlocked_responses)) + else: + return routput(choice(self.unknown_responses)) + else: + self.db.hard = True; + return routput(choice(self.cant_hear_responses)) + + def get_display_desc(self, looker, **kwargs): + # Use this for random information instead of self.db.desc + response = "In a shape of a bald goblin, the brass door knocker in the center of the red door" + if self.has("brass ring"): + response += " holds a ring in its mouth. " + response += "[You think it|You are sure it|You could've sworn it|It] [just|] winked at you." + else: + response += " [smiles|looks] at you expectantly. " + + return routput(response) + + def at_desc(self, looker): + return "what" # self.get_display_desc(looker) + + def get_display_things(self, looker): + return "" + + + def msg(self, text=None, from_obj=None, **kwargs): + "Custom msg() method reacting to say." + if from_obj != self: + # make sure to not repeat what we ourselves said or we'll create a loop + is_say = False + is_whisper = False + + try: + # debug(f"text[0]: {text[0]}, text[1]: {text[1]}") + # if text comes from a say, `text` is `('say_text', {'type': 'say'})` + say_text, is_say = text[0], text[1]['type'] == 'say' + is_whisper = text[1]['type'] == 'whisper' + except Exception: + pass + + if is_whisper: + self.at_say("I'm a little hard of hearing, can you speak up?") + elif is_say: + # First get the response (if any) + response = self.at_heard_say(say_text, from_obj) + # If there is a response + if response != None: + self.at_say(response) + + # this is needed if anyone ever puppets this NPC - without it you would never + # get any feedback from the server (not even the results of look) + super().msg(text=text, from_obj=from_obj, **kwargs) + + def at_object_leave(self, obj, target_location, **kwargs): + if obj.key == "brass ring" and target_location != self: + self.at_say("Oh my, that feels better.") + else: + self.at_say("Umph") + return True diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/support.py b/utils/support.py new file mode 100755 index 0000000..b1cdd1b --- /dev/null +++ b/utils/support.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +from typeclasses.characters import Character + +def character_has(holder, item): + """ + Return true if character, holder, has an item. + Where item is probably a string name to match an item's key. + It can also be a type, for instance: + + character_has(character, typeclasses.drinkables.TeaCup) + """ + for i in holder.contents: + if isinstance(item, str) and i.key == item: + return i + elif i.key == item: + return i + elif type(i) == item: + return i + elif i == item: + return i + +def debug(msg, sender=None, location=None): + dabbler = Character.objects.search_object("Dabbler")[0] + full_msg = "DEBUG:" + if sender: + full_msg += f" (from {sender})" + if location: + full_msg += f" (at {location})" + full_msg += f" {msg}" + dabbler.msg(full_msg) diff --git a/utils/word_list.py b/utils/word_list.py index e7faa5c..3214b15 100755 --- a/utils/word_list.py +++ b/utils/word_list.py @@ -5,9 +5,10 @@ import re import string class Token(): - def __init__(self, string): - cleaned_words = re.sub(r"[\.,\?!'\"=:;#\&\*\(\)\[\]\{\}]*", "", string) - self.words = cleaned_words.split() + def __init__(self, s): + # cleaned_words = re.sub(r"%s" % string.punctuation, "", s) + cleaned_words = re.sub(r"[\.,\?!'\":;^`\|%#\&\*<=>\(\)\[\]\{\}\+\/_-]*", "", s) + self.words = [word for word in cleaned_words.split() if word not in ["the", "a", "an"]] def empty(self): if len(self.words) == 0: @@ -19,27 +20,11 @@ class Token(): if len(result) > 0: return result[0] -def character_has(holder, item): - """ - Return true if character, holder, has an item. - Where item is probably a string name to match an item's key. - It can also be a type, for instance: - - character_has(character, typeclasses.drinkables.TeaCup) - """ - for i in holder.contents: - if isinstance(item, str) and i.key == item: - return i - elif type(i) == item: - return i - elif i == item: - return i - def squish(text): "Remove series of spaces from the text." return re.sub('[ \n\t]+', ' ', text).strip() -def routput(string): +def routput(text): """ Return string with internal word choices replaced randomly. For instance, the string: @@ -52,9 +37,8 @@ def routput(string): 'This feels cozy.' 'This feels very comfortable.' """ - # string = acc = [] - for s in string.split("["): + for s in text.split("["): choices, *rest = s.split("]") choice = random.choice(choices.split('|')) acc = acc + [choice] + rest