"""
Commands for the Evscaperoom. This contains all in-room commands as well as
admin debug-commands to help development.
Gameplay commands
- `look` - custom look
- `focus` - focus on object (to perform actions on it)
- `<action> <obj>` - arbitrary interaction with focused object
- `stand` - stand on the floor, resetting any position
- `emote` - free-form emote
- `say/whisper/shout` - simple communication
Other commands
- `evscaperoom` - starts the evscaperoom top-level menu
- `help` - custom in-room help command
- `options` - set game/accessibility options
- `who` - show who's in the room with you
- `quit` - leave a room, return to menu
Admin/development commands
- `jumpstate` - jump to specific room state
- `flag` - assign a flag to an object
- `createobj` - create a room-object set up for Evscaperoom
"""
import re
from django.conf import settings
from evennia import (
SESSION_HANDLER,
CmdSet,
Command,
InterruptCommand,
default_cmds,
syscmdkeys,
)
from evennia.utils import variable_from_module
from .utils import create_evscaperoom_object
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
_RE_ARGSPLIT = re.compile(r"\s(with|on|to|in|at)\s", re.I + re.U)
_RE_EMOTE_SPEECH = re.compile(r"(\".*?\")|(\'.*?\')")
_RE_EMOTE_NAME = re.compile(r"(/\w+)")
_RE_EMOTE_PROPER_END = re.compile(r"\.$|\.[\'\"]$|\![\'\"]$|\?[\'\"]$")
# configurable help
if hasattr(settings, "EVSCAPEROOM_HELP_SUMMARY_TEXT"):
_HELP_SUMMARY_TEXT = settings.EVSCAPEROOM_HELP_SUMMARY_TEXT
else:
_HELP_SUMMARY_TEXT = """
|yWhat to do ...|n
- Your goal is to |wescape|n the room. To do that you need to |wlook|n at
your surroundings for clues on how to escape. When you find something
interesting, |wexamine|n it for any actions you could take with it.
|yHow to explore ...|n
- |whelp [obj or command]|n - get usage help (never puzzle-related)
- |woptions|n - set game/accessibility options
- |wlook/l [obj]|n - give things a cursory look.
- |wexamine/ex/e [obj]|n - look closer at an object. Use again to
look away.
- |wstand|n - stand up if you were sitting, lying etc.
|yHow to express yourself ...|n
- |wwho [all]|n - show who's in the room or on server.
- |wemote/pose/: <something>|n - free-form emote. Use /me to refer
to yourself and /name to refer to other
things/players. Use quotes "..." to speak.
- |wsay/; <something>|n - quick-speak your mind
- |wwhisper <something>|n - whisper your mind
- |wshout <something>|n - shout your mind
|yHow to quit like a little baby ...|n
- |wquit / give up|n - admit defeat and give up
"""
_HELP_FALLBACK_TEXT = """
There is no help to be had about |y{this}|n. To look away, use |wexamine|n on
its own or with another object you are interested in.
"""
_QUIT_WARNING = """
|rDo you really want to quit?|n
{warning}
Enter |w'quit'|n again to truly give up.
"""
_QUIT_WARNING_CAN_COME_BACK = """
(Since you are not the last person to leave this room, you |gcan|n get back in here
by joining room '|c{roomname}|n' from the menu. Note however that if you leave
now, any personal achievements you may have gathered so far will be |rlost|n!)
"""
_QUIT_WARNING_LAST_CHAR = """
(You are the |rlast|n player to leave this room ('|c{roomname}|n'). This means that when you
leave, this room will go away and you |rwon't|n be able to come back to it!)
"""
[docs]class CmdEvscapeRoom(Command):
"""
Base command parent for all Evscaperoom commands.
This operates on the premise of 'focus' - the 'focus'
is set on the caller, then subsequent commands will
operate on that focus. If no focus is set,
the operation will be general or on the room.
Syntax:
command [<obj1>|<arg1>] [<prep> <obj2>|<arg2>]
"""
# always separate the command from any args with a space
arg_regex = r"(/\w+?(\s|$))|\s|$"
help_category = "Evscaperoom"
# these flags allow child classes to determine how strict parsing for obj1/obj2 should be
# (if they are given at all):
# True - obj1/obj2 must be found as Objects, otherwise it's an error aborting command
# False - obj1/obj2 will remain None, instead self.arg1, arg2 will be stored as strings
# None - if obj1/obj2 are found as Objects, set them, otherwise set arg1/arg2 as strings
obj1_search = None
obj2_search = None
def _search(self, query, required):
"""
This implements the various search modes
Args:
query (str): The search query
required (bool or None): This defines if query *must* be
found to match a single local Object or not. If None,
a non-match means returning the query unchanged. When
False, immediately return the query. If required is False,
don't search at all.
Return:
match (Object or str): The match or the search string depending
on the `required` mode.
Raises:
InterruptCommand: Aborts the command quietly.
Notes:
The _AT_SEARCH_RESULT function will handle all error messaging
for us.
"""
if required is False:
return None, query
matches = self.caller.search(query, quiet=True)
if not matches or len(matches) > 1:
if required:
if not query:
self.caller.msg("You must give an argument.")
else:
_AT_SEARCH_RESULT(matches, self.caller, query=query)
raise InterruptCommand
else:
return None, query
else:
return matches[0], None
[docs] def parse(self):
"""
Parse incoming arguments for use in all child classes.
"""
caller = self.caller
self.args = self.args.strip()
# splits to either ['obj'] or e.g. ['obj', 'on', 'obj']
parts = [part.strip() for part in _RE_ARGSPLIT.split(" " + self.args, 1)]
nparts = len(parts)
self.obj1 = None
self.arg1 = None
self.prep = None
self.obj2 = None
self.arg2 = None
if nparts == 1:
self.obj1, self.arg1 = self._search(parts[0], self.obj1_search)
elif nparts == 3:
obj1, self.prep, obj2 = parts
self.obj1, self.arg1 = self._search(obj1, self.obj1_search)
self.obj2, self.arg2 = self._search(obj2, self.obj2_search)
self.room = caller.location
self.roomstate = self.room.db.state
@property
def focus(self):
return self.caller.attributes.get("focus", category=self.room.db.tagcategory)
@focus.setter
def focus(self, obj):
self.caller.attributes.add("focus", obj, category=self.room.tagcategory)
@focus.deleter
def focus(self):
self.caller.attributes.remove("focus", category=self.room.tagcategory)
[docs]class CmdGiveUp(CmdEvscapeRoom):
"""
Give up
Usage:
give up
Abandons your attempts at escaping and of ever winning the pie-eating contest.
"""
key = "give up"
aliases = ("abort", "chicken out", "quit", "q")
[docs] def func(self):
from .menu import run_evscaperoom_menu
nchars = len(self.room.get_all_characters())
if nchars == 1:
warning = _QUIT_WARNING_LAST_CHAR.format(roomname=self.room.name)
warning = _QUIT_WARNING.format(warning=warning)
else:
warning = _QUIT_WARNING_CAN_COME_BACK.format(roomname=self.room.name)
warning = _QUIT_WARNING.format(warning=warning)
ret = yield (warning)
if ret.upper() == "QUIT":
self.msg("|R ... Oh. Okay then. Off you go.|n\n")
yield (1)
self.room.log(f"QUIT: {self.caller.key} used the quit command")
# manually call move hooks
self.room.msg_room(self.caller, f"|r{self.caller.key} gave up and was whisked away!|n")
self.room.at_object_leave(self.caller, self.caller.home)
self.caller.move_to(
self.caller.home, quiet=True, move_hooks=False, move_type="teleport"
)
# back to menu
run_evscaperoom_menu(self.caller)
else:
self.msg("|gYou're staying? That's the spirit!|n")
[docs]class CmdLook(CmdEvscapeRoom):
"""
Look at the room, an object or the currently focused object
Usage:
look [obj]
"""
key = "look"
aliases = ["l", "ls"]
obj1_search = None
obj2_search = None
[docs] def func(self):
caller = self.caller
target = self.obj1 or self.obj2 or self.focus or self.room
# the at_look hook will in turn call return_appearance and
# pass the 'unfocused' kwarg to it
txt = caller.at_look(target, unfocused=(target and target != self.focus))
self.room.msg_char(caller, txt, client_type="look")
[docs]class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
"""
List other players in the game.
Usage:
who
who all
Show who is in the room with you, or (with who all), who is online on the
server as a whole.
"""
key = "who"
obj1_search = False
obj2_search = False
[docs] def func(self):
caller = self.caller
if self.args == "all":
table = self.style_table("|wName", "|wRoom")
sessions = SESSION_HANDLER.get_sessions()
for session in sessions:
puppet = session.get_puppet()
if puppet:
location = puppet.location
locname = location.key if location else "(Outside somewhere)"
table.add_row(puppet, locname)
else:
account = session.get_account()
table.add_row(account.get_display_name(caller), "(OOC)")
txt = (
f"|cPlayers active on this server|n:\n{table}\n"
"(use 'who' to see only those in your room)"
)
else:
chars = [
f"{obj.get_display_name(caller)} - {obj.db.desc.strip()}"
for obj in self.room.get_all_characters()
if obj != caller
]
chars = "\n".join([f"{caller.key} - {caller.db.desc.strip()} (you)"] + chars)
txt = f"|cPlayers in this room (room-name '{self.room.name}')|n:\n {chars}"
caller.msg(txt)
[docs]class CmdSpeak(Command):
"""
Perform an communication action.
Usage:
say <text>
whisper
shout
"""
key = "say"
aliases = [";", "shout", "whisper"]
arg_regex = r"\w|\s|$"
[docs] def func(self):
args = self.args.strip()
caller = self.caller
action = self.cmdname
action = "say" if action == ";" else action
room = self.caller.location
if not self.args:
caller.msg(f"What do you want to {action}?")
return
if action == "shout":
args = f"|c{args.upper()}|n"
elif action == "whisper":
args = f"|C({args})|n"
else:
args = f"|c{args}|n"
message = f"~You ~{action}: {args}"
if hasattr(room, "msg_room"):
room.msg_room(caller, message)
room.log(f"{action} by {caller.key}: {args}")
[docs]class CmdEmote(Command):
"""
Perform a free-form emote. Use /me to
include yourself in the emote and /name
to include other objects or characters.
Use "..." to enact speech.
Usage:
emote <emote>
:<emote
Example:
emote /me smiles at /peter
emote /me points to /box and /lever.
"""
key = "emote"
aliases = [":", "pose"]
arg_regex = r"\w|\s|$"
[docs] def you_replace(match):
return match
[docs] def room_replace(match):
return match
[docs] def func(self):
emote = self.args.strip()
if not emote:
self.caller.msg('Usage: emote /me points to /door, saying "look over there!"')
return
speech_clr = "|c"
obj_clr = "|y"
self_clr = "|g"
player_clr = "|b"
add_period = not _RE_EMOTE_PROPER_END.search(emote)
emote = _RE_EMOTE_SPEECH.sub(speech_clr + r"\1\2|n", emote)
room = self.caller.location
characters = room.get_all_characters()
logged = False
for target in characters:
txt = []
self_refer = False
for part in _RE_EMOTE_NAME.split(emote):
nameobj = None
if part.startswith("/"):
name = part[1:]
if name == "me":
nameobj = self.caller
self_refer = True
else:
match = self.caller.search(name, quiet=True)
if len(match) == 1:
nameobj = match[0]
if nameobj:
if target == nameobj:
part = f"{self_clr}{nameobj.get_display_name(target)}|n"
elif nameobj in characters:
part = f"{player_clr}{nameobj.get_display_name(target)}|n"
else:
part = f"{obj_clr}{nameobj.get_display_name(target)}|n"
txt.append(part)
if not self_refer:
if target == self.caller:
txt = [f"{self_clr}{self.caller.get_display_name(target)}|n "] + txt
else:
txt = [f"{player_clr}{self.caller.get_display_name(target)}|n "] + txt
txt = "".join(txt).strip() + ("." if add_period else "")
if not logged and hasattr(self.caller.location, "log"):
self.caller.location.log(f"emote: {txt}")
logged = True
target.msg(txt)
[docs]class CmdFocus(CmdEvscapeRoom):
"""
Focus your attention on a target.
Usage:
focus <obj>
Once focusing on an object, use look to get more information about how it
looks and what actions is available.
"""
key = "focus"
aliases = ["examine", "e", "ex", "unfocus"]
obj1_search = None
[docs] def func(self):
if self.obj1:
old_focus = self.focus
if hasattr(old_focus, "at_unfocus"):
old_focus.at_unfocus(self.caller)
if not hasattr(self.obj1, "at_focus"):
self.caller.msg("Nothing of interest there.")
return
if self.focus != self.obj1:
self.room.msg_room(
self.caller, f"~You ~examine *{self.obj1.key}.", skip_caller=True
)
self.focus = self.obj1
self.obj1.at_focus(self.caller)
elif not self.focus:
self.caller.msg("What do you want to focus on?")
else:
old_focus = self.focus
del self.focus
self.caller.msg(f"You no longer focus on |y{old_focus.key}|n.")
[docs]class CmdOptions(CmdEvscapeRoom):
"""
Start option menu
Usage:
options
"""
key = "options"
aliases = ["option"]
[docs] def func(self):
from .menu import run_option_menu
run_option_menu(self.caller, self.session)
[docs]class CmdGet(CmdEvscapeRoom):
"""
Use focus / examine instead.
"""
key = "get"
aliases = ["inventory", "i", "inv", "give"]
[docs] def func(self):
self.caller.msg("Use |wfocus|n or |wexamine|n for handling objects.")
[docs]class CmdRerouter(default_cmds.MuxCommand):
"""
Interact with an object in focus.
Usage:
<action> [arg]
"""
# reroute commands from the default cmdset to the catch-all
# focus function where needed. This allows us to override
# individual default commands without replacing the entire
# cmdset (we want to keep most of them).
key = "open"
aliases = ["@dig", "@open"]
[docs] def func(self):
# reroute to another command
from evennia.commands import cmdhandler
cmdhandler.cmdhandler(
self.session, self.raw_string, cmdobj=CmdFocusInteraction(), cmdobj_key=self.cmdname
)
[docs]class CmdFocusInteraction(CmdEvscapeRoom):
"""
Interact with an object in focus.
Usage:
<action> [arg]
This is a special catch-all command which will operate on
the current focus. It will look for a method
`focused_object.at_focus_<action>(caller, **kwargs)` and call
it. This allows objects to just add a new hook to make that
action apply to it. The obj1, prep, obj2, arg1, arg2 are passed
as keys into the method.
"""
# all commands not matching something else goes here.
key = syscmdkeys.CMD_NOMATCH
obj1_search = None
obj2_search = None
[docs] def parse(self):
"""
We assume this type of command is always on the form `command [arg]`
"""
self.args = self.args.strip()
parts = self.args.split(None, 1)
if not self.args:
self.action, self.args = "", ""
elif len(parts) == 1:
self.action = parts[0]
self.args = ""
else:
self.action, self.args = parts
self.room = self.caller.location
[docs] def func(self):
focused = self.focus
action = self.action
if focused and hasattr(focused, f"at_focus_{action}"):
# there is a suitable hook to call!
getattr(focused, f"at_focus_{action}")(self.caller, args=self.args)
else:
self.caller.msg("Hm?")
[docs]class CmdStand(CmdEvscapeRoom):
"""
Stand up from whatever position you had.
"""
key = "stand"
[docs] def func(self):
# Positionable objects will set this flag on you.
pos = self.caller.attributes.get("position", category=self.room.tagcategory)
if pos:
# we have a position, clean up.
obj, position = pos
self.caller.attributes.remove("position", category=self.room.tagcategory)
del obj.db.positions[self.caller]
self.room.msg_room(self.caller, "~You ~are back standing on the floor again.")
else:
self.caller.msg("You are already standing.")
[docs]class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
"""
Get help.
Usage:
help <topic> or <command>
"""
key = "help"
aliases = ["?"]
[docs] def func(self):
if self.obj1:
if hasattr(self.obj1, "get_help"):
helptxt = self.obj1.get_help(self.caller)
if not helptxt:
helptxt = f"There is no help to be had about {self.obj1.get_display_name(self.caller)}."
else:
helptxt = (
f"|y{self.obj1.get_display_name(self.caller)}|n is "
"likely |rnot|n part of any of the Jester's trickery."
)
elif self.arg1:
# fall back to the normal help command
super().func()
return
else:
helptxt = _HELP_SUMMARY_TEXT
self.caller.msg(helptxt.rstrip())
# Debug/help command
[docs]class CmdCreateObj(CmdEvscapeRoom):
"""
Create command, only for Admins during debugging.
Usage:
createobj name[:typeclass]
Here, :typeclass is a class in evscaperoom.commands
"""
key = "createobj"
aliases = ["cobj"]
locks = "cmd:perm(Admin)"
obj1_search = False
obj2_search = False
[docs] def func(self):
caller = self.caller
args = self.args
if not args:
caller.msg("Usage: createobj name[:typeclass]")
return
typeclass = "EvscaperoomObject"
if ":" in args:
name, typeclass = (part.strip() for part in args.rsplit(":", 1))
if typeclass.startswith("state_"):
# a state class
typeclass = "evscaperoom.states." + typeclass
else:
name = args.strip()
obj = create_evscaperoom_object(typeclass=typeclass, key=name, location=self.room)
caller.msg(f"Created new object {name} ({obj.typeclass_path}).")
[docs]class CmdSetFlag(CmdEvscapeRoom):
"""
Assign a flag to an object. Admin use only
Usage:
flag <obj> with <flagname>
"""
key = "flag"
aliases = ["setflag"]
locks = "cmd:perm(Admin)"
obj1_search = True
obj2_search = False
[docs] def func(self):
if not self.arg2:
self.caller.msg("Usage: flag <obj> with <flagname>")
return
if hasattr(self.obj1, "set_flag"):
if self.obj1.check_flag(self.arg2):
self.obj1.unset_flag(self.arg2)
self.caller.msg(f"|rUnset|n flag '{self.arg2}' on {self.obj1}.")
else:
self.obj1.set_flag(self.arg2)
self.caller.msg(f"|gSet|n flag '{self.arg2}' on {self.obj1}.")
else:
self.caller.msg(f"Cannot set flag on {self.obj1}.")
[docs]class CmdJumpState(CmdEvscapeRoom):
"""
Jump to a given state.
Args:
jumpstate <statename>
"""
key = "jumpstate"
locks = "cmd:perm(Admin)"
obj1_search = False
obj2_search = False
[docs] def func(self):
self.caller.msg(f"Trying to move to state {self.args}")
self.room.next_state(self.args)
# Helper command to start the Evscaperoom menu
[docs]class CmdEvscapeRoomStart(Command):
"""
Go to the Evscaperoom start menu
"""
key = "evscaperoom"
help_category = "EvscapeRoom"
[docs] def func(self):
# need to import here to break circular import
from .menu import run_evscaperoom_menu
run_evscaperoom_menu(self.caller)
# command sets
[docs]class CmdSetEvScapeRoom(CmdSet):
priority = 1
[docs] def at_cmdset_creation(self):
self.add(CmdHelp())
self.add(CmdLook())
self.add(CmdGiveUp())
self.add(CmdFocus())
self.add(CmdSpeak())
self.add(CmdEmote())
self.add(CmdFocusInteraction())
self.add(CmdStand())
self.add(CmdWho())
self.add(CmdOptions())
# rerouters
self.add(CmdGet())
self.add(CmdRerouter())
# admin commands
self.add(CmdCreateObj())
self.add(CmdSetFlag())
self.add(CmdJumpState())