moss-n-puddles/commands/everyone.py
Howard Abrams 6cea0e122a Better 'say' command
That is easier to use.
2025-11-03 22:08:07 -08:00

787 lines
23 KiB
Python
Executable file

#!/usr/bin/env python
from random import random
from re import match, split, sub, MULTILINE
from django.core.exceptions import ObjectDoesNotExist
from evennia.commands.default.general import CmdGet, NumberedTargetCommand
from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.utils import iter_to_str, logger
from evennia.utils.evmore import EvMore
from commands.command import Command
from typeclasses.characters import Character
from typeclasses.tutorial import TutorBird, TutorialState
from utils.word_list import routput, paragraph, choices
def speech_effect(speech, verb, target, effects):
"""
Return speech after applying 'effects'.
Return a tuple for what a user thinks he said, and what
others actually here. If no effect, just return speech twice.
Effects:
- mute ... can't talk
- donkied ... target and others hear the value of effect
- sloshed ... slurred speech
To administer:
@set woman/donkied:effect = "Heehaw! ;; Heehaw, heehaw!"
"""
for effect in effects:
if effect:
if effect.db_key == 'donkied':
msg = choices(effect.value)
return (msg, msg, "bray")
return (speech, speech, verb)
class CmdUse(MuxCommand):
"""
Use an item.
The item, probably something in your inventory, could be something
in the local area.
Usage:
use <item> [= object]
This standard from the text adventurer tome is a catch-all command
allowing you to use or combine an object with another object.
"""
key = "use"
aliases = ["apply"]
rhs_split = ("=", " with ", " on ", " to ")
def func(self):
"""Call the 'do_use' method."""
if not self.args:
self.caller.msg("Use what?")
return
item = self.caller.search(self.lhs)
if item:
if self.rhs:
obj = self.caller.search(self.rhs)
else:
obj = None
if item.has_method('do_use'):
item.do_use(self.caller, obj)
else:
self.caller.msg(item.db.use_msg or f"You can't use {item.name}.")
class CmdPush(Command):
"""
Push an item in the area.
Usage:
push <item>
"""
key = "push"
def func(self):
"""Call the 'do_push' method."""
pusher = self.caller
to_push = self.args.strip()
if to_push == "":
pusher.msg("What do you want to push?")
return
item = pusher.search(self.args.strip())
if item:
if item.has_method('do_push'):
item.do_push(pusher)
else:
pusher.msg(item.db.push_msg or f"You can't push {item.name}.")
class CmdPull(Command):
"""
Pull on something.
Usage:
pull <item>
"""
key = "pull"
aliases = ["yank"]
def func(self):
"""Call the 'do_pull' method."""
puller = self.caller
to_pull = self.args.strip()
if to_pull == "":
puller.msg("What do you want to pull?")
return
item = puller.search(to_pull)
if item:
if item.has_method('do_pull'):
item.do_pull(puller)
else:
puller.msg(item.db.pull_msg or f"You can't pull {item.name}.")
class CmdOpen(Command):
"""
Open something.
Usage:
open <item>
"""
key = "open"
def func(self):
"""
Call an item's 'do_open' method.
Note that if the room the caller is in has a 'do_open', we
call that first to see if the room can open something (like an
exit).
"""
opener = self.caller
room = opener.location
to_open = self.args.strip()
if to_open == "":
opener.msg("What would you like to open?")
return
if room.has_method('do_open'):
if room.do_open(opener, to_open):
return
item = opener.search(to_open)
if item:
if item.has_method('do_open'):
item.do_open(opener)
else:
opener.msg(item.db.open_msg or f"You can't open {item.name}.")
class CmdClose(Command):
"""
Close something.
Usage:
close <item>
"""
key = "close"
def func(self):
"""Call the 'do_close' method.
Note that if the room the caller is in has a 'do_close', we
call that first to see if the room can close something (like
an exit).
"""
closer = self.caller
room = closer.location
to_close = self.args.strip()
if to_close == "":
closer.msg("What would you like to close?")
return
if room.has_method('do_close'):
if room.do_close(closer, to_close):
return
item = closer.search(to_close)
if item:
if item.has_method('do_close'):
item.do_close(closer)
else:
closer.msg(item.db.close_msg or f"You can't close {item.name}.")
class CmdWhisper(MuxCommand):
"""
Speak privately as your character to another.
Usage:
whisper <character> = <message>
whisper <char1>, <char2> = <message>
Talk privately to one or more characters in your current location, without
others in the room being informed.
"""
key = "whisper"
priority = 0
locks = "cmd:all()"
rhs_split = ("=")
def func(self):
"""Implement the new 'whisper' command."""
if not self.args:
self.caller.msg("What are you whispering?")
return
if not self.rhs:
self.caller.msg("Usage: whisper <character> = <message>")
return
targets = split(r" *, *", self.lhs)
chars = [self.caller.search(target) for target in targets]
for c in chars:
if not c:
return
self.caller.msg(f"Wishering to {chars}")
full_speech = f"/Me whispers to you, \"{self.rhs}\""
send_emote(self.caller, chars, full_speech, msg_type="say",
anonymous_add=None, quiet=True)
to_list = [target.get_display_name(self) for target in chars]
full_speech = f"You whisper to {iter_to_str(to_list, endsep='and')}, \"{self.rhs}\""
self.caller.msg(full_speech, from_obj=self.caller)
class CmdSay(Command):
"""Say something to the characters in the same area.
Usage:
|gsay [[<adverb>] [to] [<character>],] <message>|n
This command has many optional strings. You see, the text within
brackets, [...] are optional, while text in <...> are to be
replaced, and this technical jargon probably means little, so
perhaps examples would be better:
|gsay Good evening.|n
|wYou say, "Good evening."|n
Or:
|gsay loudly, Good evening!|n
|wYou loudly exclaim, "Good evening!"|n
In the second example, the exclamation point changes the 'say' to
an 'exclaim'. A question mark changes 'say' to 'ask'.
Also, you gave an |wadverb|n that ends in -ly, as well as a comma.
The comma is important, as it triggers |wprocessing|n. Without
the comma, you would see:
|gs loudly Good evening!
|wYou exclaim, "loudly Good evening!"|n
You can also direct your message to a particular person (or thing),
as in:
|gsay to heron, How do you do?
|wYou ask the purple heron, "How do you do?"|n
"What can I do for you?" asks the purple heron.
Again, the comma is important, otherwise, you will see:
|gsay to heron How do you do?
|wYou ask, "to heron How do you do?"|n
The purple heron questions, "Uhm. I'm not sure how to respond."
You can replace the 'say' command with any of the following:
- shout
- scream
- respond
- reply
- yell
- ask
Because this command is so common, you can also use ", as in:
|g"What's going on?|n
"""
key = "say"
verb = "say"
aliases = ["says", "speak", "shout", "yell", "exclaim", "scream", "ask",
"reply", "respond", '"', "'"]
locks = "cmd:all()"
# don't require a space after `say/'/"`
arg_regex = None
adverb = None
target = None
def parse(self):
"""Parse the input into speech parts."""
self.phrase = self.args.strip()
m = match(r".+\?.*", self.phrase)
if m:
self.verb = "ask"
m = match(r".+!.*", self.phrase)
if m:
self.verb = "exclaim"
# Final quotes can be trimmed:
self.phrase = self.phrase.strip('"').strip("'")
m = match(r"(.*) *, *(.+)", self.phrase)
if m:
self.phrase = m.group(2)
for word in split(r" +", m.group(1)):
if word.endswith("ly") and not self.adverb:
self.adverb = word
elif word == "to":
pass
else:
self.target = self.caller.search(word)
def func(self):
"""Implement the new 'say' command with switches."""
speaker = self.caller
if not self.phrase or self.phrase == "":
speaker.msg("Say what?")
return
# If speech is empty, stop here
if not speaker.at_pre_say(self.phrase):
return
if self.cmdstring == 'scream':
self.verb = 'scream'
elif self.cmdstring == 'shout':
self.verb = 'shout'
elif self.cmdstring == 'respond':
self.verb = 'respond'
elif self.cmdstring == 'reply':
self.verb = 'reply'
elif self.cmdstring == 'yell':
self.verb = 'yell'
elif self.cmdstring == 'ask':
self.verb = 'ask'
for_me, for_others, verb = \
speech_effect(self.phrase, self.verb, speaker,
speaker.attributes.get(category="effect",
return_obj=True,
return_list=True))
self.to_puppets(speaker, for_others, self.target)
self.to_you(speaker, verb, for_me)
self.to_others(speaker, verb, for_others)
def to_you(self, speaker, verb, phrase):
"""Send 'say' message to the speaker.
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_who = ""
if self.target:
who = self.target.get_display_name(speaker)
if verb == "ask":
to_who = " " + who
else:
to_who = " to " + who
adverb = self.adverb + " " if self.adverb else ""
full_speech = f"You {adverb}{verb}{to_who}, \"{phrase}\""
speaker.msg(full_speech, from_obj=speaker)
def to_others(self, speaker, verb, phrase):
"""Use the 'send_emote' to have speaker 'say' something to others.
We use /me to be replaced by the user's appearance to others.
We also need to deal with weird English usage.
"""
to_whom = ""
if self.target:
whom = f"/{self.target.key}"
if verb == "ask":
to_whom = " " + whom
else:
to_whom = " to " + whom
adverb = self.adverb + " " if self.adverb else ""
# English is weird...
verb = 'replie' if verb == 'reply' else verb
targets = [item for item in speaker.location.contents
if item != speaker]
full_speech = f"/me {adverb}{verb}s{to_whom}, \"{phrase}\""
send_emote(speaker, targets, full_speech, msg_type="say",
anonymous_add=None)
def to_puppets(self, speaker, phrase, target=None):
"""Send phrase to puppet, target.
If target is None, then send to all puppets.
"""
# Speak to everyone in the room:
if not target:
for char in speaker.location.contents:
if hasattr(char, 'other_say') and callable(char.other_say):
char.other_say(speaker, phrase)
else:
if hasattr(target, 'other_sayto') and callable(target.other_sayto):
logger.info(f"Found {target.key}: {phrase}")
target.other_sayto(speaker, phrase)
class CmdThink(Command):
"""Think a thought out loud.
Usage:
think <message>
Similar to the 'say' or 'pose' commands, this communicates an
inner monologue to other players on the 'public' channel.
"""
key = "think"
aliases = ["thinks", "("]
arg_regex = None
def func(self):
"""Implement the think out loud command."""
if not self.args:
self.caller.msg("What do you want to think out loud?")
else:
thought = self.args.strip()
if (self.caller.db.thinking_count or 0) < 3 or random() < 0.4:
msg = routput(
f"<< thinks ^ wonders >> << out loud ^ aloud >> "
f"... o O ( {thought} )"
)
else:
msg = f". o O ( {thought} )"
self.caller.db.thinking_count = (self.caller.db.thinking_count or 0) + 1
self.caller.execute_cmd(f"pub :{msg}")
class CmdRead(Command):
"""
Return the inside contents of a book or other readable object.
Usage:
read <target>
To add something to read on target, use the @set command:
@set <target>/inside = 'This is the text to read.'
"""
key = "read"
def find_readable(self, reader, readable_str):
"""Search the room for a readable item."""
if readable_str == "chalkboard":
readable_str = "shrub"
elif readable_str.startswith("cocktail") or \
readable_str.startswith("drink") or readable_str == "list":
readable_str = "sign"
targets = reader.search(readable_str, quiet=True)
if not targets:
reader.msg(f"You don't see {readable_str}.")
return None
label_targets = [t for t in targets if t.db.inside]
if len(label_targets) == 1:
return label_targets[0]
if len(label_targets) == 0:
reader.msg(f"You can't find anything readable on {readable_str}.")
else:
reader.msg(f"Too many things match, '{readable_str}'. "
"Can you narrow it down with a title, "
"or preface with a number, like '2-paper'?")
def func(self):
"""Return the 'inside' attribute."""
reader = self.caller
target_str = self.args.strip()
if target_str == "":
reader.msg("Usage: |gread <object>|n")
return
book = self.find_readable(reader, target_str)
if book:
contents = book.db.inside
prefix = book.db.prefix
if prefix:
prefix = prefix + "|/"
if contents.startswith("file:"):
self.show_file(reader, contents[5:], prefix,
self.client_width(), reader.client_height())
else:
reader.msg((prefix or "") + contents)
def show_file(self, reader, filename, prefix, width, height):
"""
Display a file to the user.
The file is _somewhat_ Markdown formatted.
"""
with open(filename, "r") as myfile:
buf = myfile.read()
session = reader.sessions.get()[0]
width = self.client_width()
if reader.is_webclient():
tidied = md_to_html(buf)
else:
tidied = md_to_evennia(buf, reader.is_utf(), width)
if prefix:
tidied = prefix + "\n\n" + tidied
EvMore(reader, tidied, session=session,
justify=True, justify_kwargs={"width": width})
def md_to_evennia(text, utf, width):
brk = '' if utf else '-'
line_brk = '|W' + (brk * width) + '|n'
breaks = [line_brk if line.startswith("#") else line
for line in md_preprocessor(text)]
return "\n".join(breaks)
def md_to_html(text):
brk = '───────────────────────────────────────────────────────'
breaks = [brk if line.startswith("#") else line
for line in md_preprocessor(text)]
return "\n".join(breaks)
def md_preprocessor(text):
lines = text.splitlines()
return [line for line in lines if not line.startswith(">")]
class CmdTake(CmdGet, NumberedTargetCommand):
"""
Get something, possibly from another character or NPC.
Usage:
get <thing> [ from <character> ]
Note that only some things can be stolen.
For instance, the brass ring from the door knocker.
"""
key = "take"
aliases = ["steal", "get"]
rhs_split = ("=", " from ")
def func(self):
"""
Implement the take command.
Since this command is designed to work on the object, we
operate only on self.obj.
"""
if not self.args:
self.caller.msg("What do you want to get?")
elif not self.rhs:
# This is soo bad to hard-code this game logic, but enough
# people complained about not being able to 'get' the
# ring, I need to:
if self.lhs == "ring" and self.caller.location.key == "Grotto":
self.caller.msg("From whom do you want to get this ring?")
elif self.lhs == "frog" and self.caller.location.key == "Frog Meadow":
self.caller.msg("The little guys are too quick to catch.")
else:
super().func() # Call the 'get' function instead.
else:
location = self.caller.search(self.rhs, quiet=True)
if len(location) == 0:
self.caller.msg(f"Can't take '{self.lhs}' from '{self.rhs}'.")
return
location = location[0]
if location.is_typeclass("typeclasses.rooms.Room"):
self.args = self.lhs
super().func() # Call the 'get' function instead.
return
self.caller.do_take(self.lhs, location)
class CmdDrink(Command):
"""
Drink a beverage in your inventory.
Usage:
drink [ container ]
If you are holding a teacup, or cocktail, and it is not empty,
you may drink.
This doesn't tell others of this particular activity.
"""
key = "drink"
aliases = ["sip", "quaff"]
def drink_item(self, name):
"""Find item in inventory, name, and call 'do_drink' on it."""
notfound = f"You don't have {name} in your inventory."
item = self.caller.search(name, location=self.caller,
nofound_string=notfound)
if item:
if item.has_method('do_drink'):
item.do_drink(self.caller)
else:
self.caller.msg(f"The {item.name} is not drinkable.")
def drink_anything(self):
"""Drink anything in your inventory, but only if you have one thing."""
def not_empty(item):
return (item.db.amount or 0) > 0
containers = self.search_contents_by_func('do_drink', not_empty)
if len(containers) == 1:
containers[0].do_drink(self.caller)
elif len(containers) > 1:
self.caller.msg("You have too many things you can drink. "
"Which one do you want?")
else:
self.caller.msg("You have nothing to drink.")
def func(self):
"""
If given the name of something to drink, find and drink it.
Otherwise, drink the first item you find in your inventory.
"""
goal = self.args.strip()
if goal and goal != "":
self.drink_item(goal)
else:
self.drink_anything()
class CmdEat(Command):
"""
Eat something edible in your inventory.
Usage:
eat [ food-item ]
This doesn't tell others of this particular activity.
"""
key = "eat"
aliases = ["consume", "bite"]
def eat_item(self, name):
"""Find item in inventory, name, and call 'do_eat' on it."""
notfound = f"You don't have {name} in your inventory."
item = self.caller.search(name, location=self.caller,
nofound_string=notfound)
if item:
if item.has_method('do_eat'):
item.do_eat(self.caller)
else:
self.caller.msg(f"The {item.name} is not edible.")
def eat_anything(self):
"""Eat something in your inventory, but only if you have one thing."""
def not_gone(item):
return (item.db.amount or 0) > 0
items = self.search_contents_by_func('do_eat', not_gone)
if len(items) == 0:
self.caller.msg("You have nothing to eat.")
else:
item = items[0]
if len(items) > 1:
self.caller.msg(f"Eating the {item.name}.")
item.do_eat(self.caller)
def func(self):
"""
If given the name of something to eat, find and eat it.
Otherwise, eat the first item you find in your inventory.
"""
goal = self.args.strip()
if goal and goal != "":
self.eat_item(goal)
else:
self.eat_anything()
class CmdFeed(MuxCommand):
"""
Feed or give something to an object that can eat.
Typically this is used to feed wood to a fire, or
berries to a beast.
Usage:
|gfeed <target>|n
Or:
|gfeed <food> to <target>|n
Where 'food' is something you have in your inventory,
and target is a pet or something that wants to eat.
If you don't specify the 'food', the target will eat
what you might have they're interested in.
"""
key = "feed"
rhs_split = ("=", " to ")
def func(self):
"""
Implements the feed (or give) command.
"""
if not self.args:
self.caller.msg("Feed what?")
return
feeder = self.caller
if self.rhs:
eater = feeder.search(self.rhs)
food = feeder.search(self.lhs, location=feeder)
if not food:
return
else:
eater = feeder.search(self.lhs)
food = None
if eater:
if eater.has_method('feed'):
eater.feed(feeder, food)
if food:
try:
food.delete()
# Allow the eater to delete the object.
except ObjectDoesNotExist:
pass
else:
name = eater.get_display_name(feeder)
feeder.msg(f"You can't feed, {name}.")
else:
feeder.msg(f"Don't see a '{self.lhs}' to feed.")
# Pass this off to CmdGive?
#
# feeder.execute_cmd("give " + self.args)
#
# supercmd = CmdGive()
# supercmd.caller = feeder
# supercmd.args = self.args
# supercmd.lhs = self.lhs
# supercmd.rhs = self.rhs
# supercmd.func()