"""
Room Typeclasses for the TutorialWorld.
This defines special types of Rooms available in the tutorial. To keep
everything in one place we define them together with the custom
commands needed to control them. Those commands could also have been
in a separate module (e.g. if they could have been re-used elsewhere.)
"""
import random
from evennia import TICKER_HANDLER
from evennia import CmdSet, Command, DefaultRoom
from evennia import utils, create_object, search_object
from evennia import syscmdkeys, default_cmds
from evennia.contrib.tutorial_world.objects import LightSource
# the system error-handling module is defined in the settings. We load the
# given setting here using utils.object_from_module. This way we can use
# it regardless of if we change settings later.
from django.conf import settings
_SEARCH_AT_RESULT = utils.object_from_module(settings.SEARCH_AT_RESULT)
# -------------------------------------------------------------
#
# Tutorial room - parent room class
#
# This room is the parent of all rooms in the tutorial.
# It defines a tutorial command on itself (available to
# all those who are in a tutorial room).
#
# -------------------------------------------------------------
#
# Special command available in all tutorial rooms
[docs]class CmdTutorial(Command):
"""
Get help during the tutorial
Usage:
tutorial [obj]
This command allows you to get behind-the-scenes info
about an object or the current location.
"""
key = "tutorial"
aliases = ["tut"]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""
All we do is to scan the current location for an Attribute
called `tutorial_info` and display that.
"""
caller = self.caller
if not self.args:
target = self.obj # this is the room the command is defined on
else:
target = caller.search(self.args.strip())
if not target:
return
helptext = target.db.tutorial_info or ""
if helptext:
helptext = f" |G{helptext}|n"
else:
helptext = " |RSorry, there is no tutorial help available here.|n"
helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
caller.msg(helptext)
# for the @detail command we inherit from MuxCommand, since
# we want to make use of MuxCommand's pre-parsing of '=' in the
# argument.
[docs]class CmdTutorialSetDetail(default_cmds.MuxCommand):
"""
sets a detail on a room
Usage:
@detail <key> = <description>
@detail <key>;<alias>;... = description
Example:
@detail walls = The walls are covered in ...
@detail castle;ruin;tower = The distant ruin ...
This sets a "detail" on the object this command is defined on
(TutorialRoom for this tutorial). This detail can be accessed with
the TutorialRoomLook command sitting on TutorialRoom objects (details
are set as a simple dictionary on the room). This is a Builder command.
We custom parse the key for the ;-separator in order to create
multiple aliases to the detail all at once.
"""
key = "@detail"
locks = "cmd:perm(Builder)"
help_category = "TutorialWorld"
[docs] def func(self):
"""
All this does is to check if the object has
the set_detail method and uses it.
"""
if not self.args or not self.rhs:
self.caller.msg("Usage: @detail key = description")
return
if not hasattr(self.obj, "set_detail"):
self.caller.msg("Details cannot be set on %s." % self.obj)
return
for key in self.lhs.split(";"):
# loop over all aliases, if any (if not, this will just be
# the one key to loop over)
self.obj.set_detail(key, self.rhs)
self.caller.msg("Detail set: '%s': '%s'" % (self.lhs, self.rhs))
[docs]class CmdTutorialLook(default_cmds.CmdLook):
"""
looks at the room and on details
Usage:
look <obj>
look <room detail>
look *<account>
Observes your location, details at your location or objects
in your vicinity.
Tutorial: This is a child of the default Look command, that also
allows us to look at "details" in the room. These details are
things to examine and offers some extra description without
actually having to be actual database objects. It uses the
return_detail() hook on TutorialRooms for this.
"""
# we don't need to specify key/locks etc, this is already
# set by the parent.
help_category = "TutorialWorld"
[docs] def func(self):
"""
Handle the looking. This is a copy of the default look
code except for adding in the details.
"""
caller = self.caller
args = self.args
if args:
# we use quiet=True to turn off automatic error reporting.
# This tells search that we want to handle error messages
# ourself. This also means the search function will always
# return a list (with 0, 1 or more elements) rather than
# result/None.
looking_at_obj = caller.search(
args,
# note: excludes room/room aliases
candidates=caller.location.contents + caller.contents,
use_nicks=True,
quiet=True,
)
if len(looking_at_obj) != 1:
# no target found or more than one target found (multimatch)
# look for a detail that may match
detail = self.obj.return_detail(args)
if detail:
self.caller.msg(detail)
return
else:
# no detail found, delegate our result to the normal
# error message handler.
_SEARCH_AT_RESULT(looking_at_obj, caller, args)
return
else:
# we found a match, extract it from the list and carry on
# normally with the look handling.
looking_at_obj = looking_at_obj[0]
else:
looking_at_obj = caller.location
if not looking_at_obj:
caller.msg("You have no location to look at!")
return
if not hasattr(looking_at_obj, "return_appearance"):
# this is likely due to us having an account instead
looking_at_obj = looking_at_obj.character
if not looking_at_obj.access(caller, "view"):
caller.msg("Could not find '%s'." % args)
return
# get object's appearance
caller.msg(looking_at_obj.return_appearance(caller))
# the object's at_desc() method.
looking_at_obj.at_desc(looker=caller)
return
[docs]class CmdTutorialGiveUp(default_cmds.MuxCommand):
"""
Give up the tutorial-world quest and return to Limbo, the start room of the
server.
"""
key = "give up"
aliases = ["abort"]
[docs] def func(self):
outro_room = OutroRoom.objects.all()
if outro_room:
outro_room = outro_room[0]
else:
self.caller.msg(
"That didn't work (seems like a bug). "
"Try to use the |wteleport|n command instead."
)
return
self.caller.move_to(outro_room)
[docs]class TutorialRoomCmdSet(CmdSet):
"""
Implements the simple tutorial cmdset. This will overload the look
command in the default CharacterCmdSet since it has a higher
priority (ChracterCmdSet has prio 0)
"""
key = "tutorial_cmdset"
priority = 1
[docs] def at_cmdset_creation(self):
"""add the tutorial-room commands"""
self.add(CmdTutorial())
self.add(CmdTutorialSetDetail())
self.add(CmdTutorialLook())
self.add(CmdTutorialGiveUp())
[docs]class TutorialRoom(DefaultRoom):
"""
This is the base room type for all rooms in the tutorial world.
It defines a cmdset on itself for reading tutorial info about the location.
"""
[docs] def at_object_creation(self):
"""Called when room is first created"""
self.db.tutorial_info = (
"This is a tutorial room. It allows you to use the 'tutorial' command."
)
self.cmdset.add_default(TutorialRoomCmdSet)
[docs] def at_object_receive(self, new_arrival, source_location):
"""
When an object enter a tutorial room we tell other objects in
the room about it by trying to call a hook on them. The Mob object
uses this to cheaply get notified of enemies without having
to constantly scan for them.
Args:
new_arrival (Object): the object that just entered this room.
source_location (Object): the previous location of new_arrival.
"""
if new_arrival.has_account and not new_arrival.is_superuser:
# this is a character
for obj in self.contents_get(exclude=new_arrival):
if hasattr(obj, "at_new_arrival"):
obj.at_new_arrival(new_arrival)
[docs] def return_detail(self, detailkey):
"""
This looks for an Attribute "obj_details" and possibly
returns the value of it.
Args:
detailkey (str): The detail being looked at. This is
case-insensitive.
"""
details = self.db.details
if details:
return details.get(detailkey.lower(), None)
[docs] def set_detail(self, detailkey, description):
"""
This sets a new detail, using an Attribute "details".
Args:
detailkey (str): The detail identifier to add (for
aliases you need to add multiple keys to the
same description). Case-insensitive.
description (str): The text to return when looking
at the given detailkey.
"""
if self.db.details:
self.db.details[detailkey.lower()] = description
else:
self.db.details = {detailkey.lower(): description}
# -------------------------------------------------------------
#
# Weather room - room with a ticker
#
# -------------------------------------------------------------
# These are rainy weather strings
WEATHER_STRINGS = (
"The rain coming down from the iron-grey sky intensifies.",
"A gust of wind throws the rain right in your face. Despite your cloak you shiver.",
"The rainfall eases a bit and the sky momentarily brightens.",
"For a moment it looks like the rain is slowing, then it begins anew with renewed force.",
"The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.",
"The wind is picking up, howling around you, throwing water droplets in your face. It's cold.",
"Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.",
"It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.",
"Lightning strikes in several thundering bolts, striking the trees in the forest to your west.",
"You hear the distant howl of what sounds like some sort of dog or wolf.",
"Large clouds rush across the sky, throwing their load of rain over the world.",
)
[docs]class WeatherRoom(TutorialRoom):
"""
This should probably better be called a rainy room...
This sets up an outdoor room typeclass. At irregular intervals,
the effects of weather will show in the room. Outdoor rooms should
inherit from this.
"""
[docs] def at_object_creation(self):
"""
Called when object is first created.
We set up a ticker to update this room regularly.
Note that we could in principle also use a Script to manage
the ticking of the room; the TickerHandler works fine for
simple things like this though.
"""
super().at_object_creation()
# subscribe ourselves to a ticker to repeatedly call the hook
# "update_weather" on this object. The interval is randomized
# so as to not have all weather rooms update at the same time.
self.db.interval = random.randint(50, 70)
TICKER_HANDLER.add(
interval=self.db.interval, callback=self.update_weather, idstring="tutorial"
)
# this is parsed by the 'tutorial' command on TutorialRooms.
self.db.tutorial_info = "This room has a Script running that has it echo a weather-related message at irregular intervals."
[docs] def update_weather(self, *args, **kwargs):
"""
Called by the tickerhandler at regular intervals. Even so, we
only update 20% of the time, picking a random weather message
when we do. The tickerhandler requires that this hook accepts
any arguments and keyword arguments (hence the *args, **kwargs
even though we don't actually use them in this example)
"""
if random.random() < 0.2:
# only update 20 % of the time
self.msg_contents("|w%s|n" % random.choice(WEATHER_STRINGS))
SUPERUSER_WARNING = (
"\nWARNING: You are playing as a superuser ({name}). Use the {quell} command to\n"
"play without superuser privileges (many functions and puzzles ignore the \n"
"presence of a superuser, making this mode useful for exploring things behind \n"
"the scenes later).\n"
)
# ------------------------------------------------------------
#
# Intro Room - unique room
#
# This room marks the start of the tutorial. It sets up properties on
# the player char that is needed for the tutorial.
#
# -------------------------------------------------------------
[docs]class CmdEvenniaIntro(Command):
"""
Start the Evennia intro wizard.
Usage:
intro
"""
key = "intro"
[docs] def func(self):
from .intro_menu import init_menu
# quell also superusers
if self.caller.account:
self.caller.account.execute_cmd("quell")
self.caller.msg("(Auto-quelling)")
init_menu(self.caller)
[docs]class CmdSetEvenniaIntro(CmdSet):
key = "Evennia Intro StartSet"
[docs] def at_cmdset_creation(self):
self.add(CmdEvenniaIntro())
[docs]class IntroRoom(TutorialRoom):
"""
Intro room
properties to customize:
char_health - integer > 0 (default 20)
"""
[docs] def at_object_creation(self):
"""
Called when the room is first created.
"""
super().at_object_creation()
self.db.tutorial_info = (
"The first room of the tutorial. "
"This assigns the health Attribute to "
"the account."
)
self.cmdset.add(CmdSetEvenniaIntro, permanent=True)
[docs] def at_object_receive(self, character, source_location):
"""
Assign properties on characters
"""
# setup character for the tutorial
health = self.db.char_health or 20
if character.has_account:
character.db.health = health
character.db.health_max = health
if character.is_superuser:
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
else:
# quell user
if character.account:
character.account.execute_cmd("quell")
character.msg("(Auto-quelling while in tutorial-world)")
# -------------------------------------------------------------
#
# Bridge - unique room
#
# Defines a special west-eastward "bridge"-room, a large room that takes
# several steps to cross. It is complete with custom commands and a
# chance of falling off the bridge. This room has no regular exits,
# instead the exitings are handled by custom commands set on the account
# upon first entering the room.
#
# Since one can enter the bridge room from both ends, it is
# divided into five steps:
# westroom <- 0 1 2 3 4 -> eastroom
#
# -------------------------------------------------------------
[docs]class CmdEast(Command):
"""
Go eastwards across the bridge.
Tutorial info:
This command relies on the caller having two Attributes
(assigned by the room when entering):
- east_exit: a unique name or dbref to the room to go to
when exiting east.
- west_exit: a unique name or dbref to the room to go to
when exiting west.
The room must also have the following Attributes
- tutorial_bridge_posistion: the current position on
on the bridge, 0 - 4.
"""
key = "east"
aliases = ["e"]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""move one step eastwards"""
caller = self.caller
bridge_step = min(5, caller.db.tutorial_bridge_position + 1)
if bridge_step > 4:
# we have reached the far east end of the bridge.
# Move to the east room.
eexit = search_object(self.obj.db.east_exit)
if eexit:
caller.move_to(eexit[0])
else:
caller.msg("No east exit was found for this room. Contact an admin.")
return
caller.db.tutorial_bridge_position = bridge_step
# since we are really in one room, we have to notify others
# in the room when we move.
caller.location.msg_contents(
"%s steps eastwards across the bridge." % caller.name, exclude=caller
)
caller.execute_cmd("look")
# go back across the bridge
[docs]class CmdWest(Command):
"""
Go westwards across the bridge.
Tutorial info:
This command relies on the caller having two Attributes
(assigned by the room when entering):
- east_exit: a unique name or dbref to the room to go to
when exiting east.
- west_exit: a unique name or dbref to the room to go to
when exiting west.
The room must also have the following property:
- tutorial_bridge_posistion: the current position on
on the bridge, 0 - 4.
"""
key = "west"
aliases = ["w"]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""move one step westwards"""
caller = self.caller
bridge_step = max(-1, caller.db.tutorial_bridge_position - 1)
if bridge_step < 0:
# we have reached the far west end of the bridge.
# Move to the west room.
wexit = search_object(self.obj.db.west_exit)
if wexit:
caller.move_to(wexit[0])
else:
caller.msg("No west exit was found for this room. Contact an admin.")
return
caller.db.tutorial_bridge_position = bridge_step
# since we are really in one room, we have to notify others
# in the room when we move.
caller.location.msg_contents(
"%s steps westwards across the bridge." % caller.name, exclude=caller
)
caller.execute_cmd("look")
BRIDGE_POS_MESSAGES = (
"You are standing |wvery close to the the bridge's western foundation|n."
" If you go west you will be back on solid ground ...",
"The bridge slopes precariously where it extends eastwards"
" towards the lowest point - the center point of the hang bridge.",
"You are |whalfways|n out on the unstable bridge.",
"The bridge slopes precariously where it extends westwards"
" towards the lowest point - the center point of the hang bridge.",
"You are standing |wvery close to the bridge's eastern foundation|n."
" If you go east you will be back on solid ground ...",
)
BRIDGE_MOODS = (
"The bridge sways in the wind.",
"The hanging bridge creaks dangerously.",
"You clasp the ropes firmly as the bridge sways and creaks under you.",
"From the castle you hear a distant howling sound, like that of a large dog or other beast.",
"The bridge creaks under your feet. Those planks does not seem very sturdy.",
"Far below you the ocean roars and throws its waves against the cliff,"
" as if trying its best to reach you.",
"Parts of the bridge come loose behind you, falling into the chasm far below!",
"A gust of wind causes the bridge to sway precariously.",
"Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...",
"The section of rope you hold onto crumble in your hands,"
" parts of it breaking apart. You sway trying to regain balance.",
)
FALL_MESSAGE = (
"Suddenly the plank you stand on gives way under your feet! You fall!"
"\nYou try to grab hold of an adjoining plank, but all you manage to do is to "
"divert your fall westwards, towards the cliff face. This is going to hurt ... "
"\n ... The world goes dark ...\n\n"
)
[docs]class CmdLookBridge(Command):
"""
looks around at the bridge.
Tutorial info:
This command assumes that the room has an Attribute
"fall_exit", a unique name or dbref to the place they end upp
if they fall off the bridge.
"""
key = "look"
aliases = ["l"]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""Looking around, including a chance to fall."""
caller = self.caller
bridge_position = self.caller.db.tutorial_bridge_position
# this command is defined on the room, so we get it through self.obj
location = self.obj
# randomize the look-echo
message = "|c%s|n\n%s\n%s" % (
location.key,
BRIDGE_POS_MESSAGES[bridge_position],
random.choice(BRIDGE_MOODS),
)
chars = [obj for obj in self.obj.contents_get(exclude=caller) if obj.has_account]
if chars:
# we create the You see: message manually here
message += "\n You see: %s" % ", ".join("|c%s|n" % char.key for char in chars)
self.caller.msg(message)
# there is a chance that we fall if we are on the western or central
# part of the bridge.
if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser:
# we fall 5% of time.
fall_exit = search_object(self.obj.db.fall_exit)
if fall_exit:
self.caller.msg("|r%s|n" % FALL_MESSAGE)
self.caller.move_to(fall_exit[0], quiet=True)
# inform others on the bridge
self.obj.msg_contents(
"A plank gives way under %s's feet and "
"they fall from the bridge!" % self.caller.key
)
# custom help command
[docs]class CmdBridgeHelp(Command):
"""
Overwritten help command while on the bridge.
"""
key = "help"
aliases = ["h", "?"]
locks = "cmd:all()"
help_category = "Tutorial world"
[docs] def func(self):
"""Implements the command."""
string = (
"You are trying hard not to fall off the bridge ..."
"\n\nWhat you can do is trying to cross the bridge |weast|n"
" or try to get back to the mainland |wwest|n)."
)
self.caller.msg(string)
[docs]class BridgeCmdSet(CmdSet):
"""This groups the bridge commands. We will store it on the room."""
key = "Bridge commands"
priority = 2 # this gives it precedence over the normal look/help commands.
[docs] def at_cmdset_creation(self):
"""Called at first cmdset creation"""
self.add(CmdTutorial())
self.add(CmdEast())
self.add(CmdWest())
self.add(CmdLookBridge())
self.add(CmdBridgeHelp())
BRIDGE_WEATHER = (
"The rain intensifies, making the planks of the bridge even more slippery.",
"A gust of wind throws the rain right in your face.",
"The rainfall eases a bit and the sky momentarily brightens.",
"The bridge shakes under the thunder of a closeby thunder strike.",
"The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.",
"The wind is picking up, howling around you and causing the bridge to sway from side to side.",
"Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.",
"The bridge sways from side to side in the wind.",
"Below you a particularly large wave crashes into the rocks.",
"From the ruin you hear a distant, otherwordly howl. Or maybe it was just the wind.",
)
[docs]class BridgeRoom(WeatherRoom):
"""
The bridge room implements an unsafe bridge. It also enters the player into
a state where they get new commands so as to try to cross the bridge.
We want this to result in the account getting a special set of
commands related to crossing the bridge. The result is that it
will take several steps to cross it, despite it being represented
by only a single room.
We divide the bridge into steps:
self.db.west_exit - - | - - self.db.east_exit
0 1 2 3 4
The position is handled by a variable stored on the character
when entering and giving special move commands will
increase/decrease the counter until the bridge is crossed.
We also has self.db.fall_exit, which points to a gathering
location to end up if we happen to fall off the bridge (used by
the CmdLookBridge command).
"""
[docs] def at_object_creation(self):
"""Setups the room"""
# this will start the weather room's ticker and tell
# it to call update_weather regularly.
super().at_object_creation()
# this identifies the exits from the room (should be the command
# needed to leave through that exit). These are defaults, but you
# could of course also change them after the room has been created.
self.db.west_exit = "cliff"
self.db.east_exit = "gate"
self.db.fall_exit = "cliffledge"
# add the cmdset on the room.
self.cmdset.add(BridgeCmdSet, permanent=True)
# since the default Character's at_look() will access the room's
# return_description (this skips the cmdset) when
# first entering it, we need to explicitly turn off the room
# as a normal view target - once inside, our own look will
# handle all return messages.
self.locks.add("view:false()")
[docs] def update_weather(self, *args, **kwargs):
"""
This is called at irregular intervals and makes the passage
over the bridge a little more interesting.
"""
if random.random() < 80:
# send a message most of the time
self.msg_contents("|w%s|n" % random.choice(BRIDGE_WEATHER))
[docs] def at_object_receive(self, character, source_location):
"""
This hook is called by the engine whenever the player is moved
into this room.
"""
if character.has_account:
# we only run this if the entered object is indeed a player object.
# check so our east/west exits are correctly defined.
wexit = search_object(self.db.west_exit)
eexit = search_object(self.db.east_exit)
fexit = search_object(self.db.fall_exit)
if not (wexit and eexit and fexit):
character.msg(
"The bridge's exits are not properly configured. "
"Contact an admin. Forcing west-end placement."
)
character.db.tutorial_bridge_position = 0
return
if source_location == eexit[0]:
# we assume we enter from the same room we will exit to
character.db.tutorial_bridge_position = 4
else:
# if not from the east, then from the west!
character.db.tutorial_bridge_position = 0
character.execute_cmd("look")
[docs] def at_object_leave(self, character, target_location):
"""
This is triggered when the player leaves the bridge room.
"""
if character.has_account:
# clean up the position attribute
del character.db.tutorial_bridge_position
# -------------------------------------------------------------------------------
#
# Dark Room - a room with states
#
# This room limits the movemenets of its denizens unless they carry an active
# LightSource object (LightSource is defined in
# tutorialworld.objects.LightSource)
#
# -------------------------------------------------------------------------------
DARK_MESSAGES = (
"It is pitch black. You are likely to be eaten by a grue.",
"It's pitch black. You fumble around but cannot find anything.",
"You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
"You don't see a thing! Blindly grasping the air around you, you find nothing.",
"It's totally dark here. You almost stumble over some un-evenness in the ground.",
"You are completely blind. For a moment you think you hear someone breathing nearby ... "
"\n ... surely you must be mistaken.",
"Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
"Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation,"
" but its too damp to burn.",
"You can't see anything, but the air is damp. It feels like you are far underground.",
)
ALREADY_LIGHTSOURCE = (
"You don't want to stumble around in blindness anymore. You already "
"found what you need. Let's get light already!"
)
FOUND_LIGHTSOURCE = (
"Your fingers bump against a splinter of wood in a corner."
" It smells of resin and seems dry enough to burn! "
"You pick it up, holding it firmly. Now you just need to"
" |wlight|n it using the flint and steel you carry with you."
)
[docs]class CmdLookDark(Command):
"""
Look around in darkness
Usage:
look
Look around in the darkness, trying
to find something.
"""
key = "look"
aliases = ["l", "feel", "search", "feel around", "fiddle"]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""
Implement the command.
This works both as a look and a search command; there is a
random chance of eventually finding a light source.
"""
caller = self.caller
# count how many searches we've done
nr_searches = caller.ndb.dark_searches
if nr_searches is None:
nr_searches = 0
caller.ndb.dark_searches = nr_searches
if nr_searches < 4 and random.random() < 0.90:
# we don't find anything
caller.msg(random.choice(DARK_MESSAGES))
caller.ndb.dark_searches += 1
else:
# we could have found something!
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
# we already carry a LightSource object.
caller.msg(ALREADY_LIGHTSOURCE)
else:
# don't have a light source, create a new one.
create_object(LightSource, key="splinter", location=caller)
caller.msg(FOUND_LIGHTSOURCE)
[docs]class CmdDarkHelp(Command):
"""
Help command for the dark state.
"""
key = "help"
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""
Replace the the help command with a not-so-useful help
"""
string = (
"Can't help you until you find some light! Try looking/feeling around for something to burn. "
"You shouldn't give up even if you don't find anything right away."
)
self.caller.msg(string)
[docs]class CmdDarkNoMatch(Command):
"""
This is a system command. Commands with special keys are used to
override special sitations in the game. The CMD_NOMATCH is used
when the given command is not found in the current command set (it
replaces Evennia's default behavior or offering command
suggestions)
"""
key = syscmdkeys.CMD_NOMATCH
locks = "cmd:all()"
[docs] def func(self):
"""Implements the command."""
self.caller.msg(
"Until you find some light, there's not much you can do. "
"Try feeling around, maybe you'll find something helpful!"
)
[docs]class DarkCmdSet(CmdSet):
"""
Groups the commands of the dark room together. We also import the
default say command here so that players can still talk in the
darkness.
We give the cmdset the mergetype "Replace" to make sure it
completely replaces whichever command set it is merged onto
(usually the default cmdset)
"""
key = "darkroom_cmdset"
mergetype = "Replace"
priority = 2
[docs] def at_cmdset_creation(self):
"""populate the cmdset."""
self.add(CmdTutorial())
self.add(CmdLookDark())
self.add(CmdDarkHelp())
self.add(CmdDarkNoMatch())
self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdQuit())
self.add(default_cmds.CmdHome())
[docs]class DarkRoom(TutorialRoom):
"""
A dark room. This tries to start the DarkState script on all
objects entering. The script is responsible for making sure it is
valid (that is, that there is no light source shining in the room).
The is_lit Attribute is used to define if the room is currently lit
or not, so as to properly echo state changes.
Since this room (in the tutorial) is meant as a sort of catch-all,
we also make sure to heal characters ending up here, since they
may have been beaten up by the ghostly apparition at this point.
"""
[docs] def at_object_creation(self):
"""
Called when object is first created.
"""
super().at_object_creation()
self.db.tutorial_info = "This is a room with custom command sets on itself."
# the room starts dark.
self.db.is_lit = False
self.cmdset.add(DarkCmdSet, permanent=True)
[docs] def at_init(self):
"""
Called when room is first recached (such as after a reload)
"""
self.check_light_state()
def _carries_light(self, obj):
"""
Checks if the given object carries anything that gives light.
Note that we do NOT look for a specific LightSource typeclass,
but for the Attribute is_giving_light - this makes it easy to
later add other types of light-giving items. We also accept
if there is a light-giving object in the room overall (like if
a splinter was dropped in the room)
"""
return (
obj.is_superuser
or obj.db.is_giving_light
or any(o for o in obj.contents if o.db.is_giving_light)
)
def _heal(self, character):
"""
Heal a character.
"""
health = character.db.health_max or 20
character.db.health = health
[docs] def check_light_state(self, exclude=None):
"""
This method checks if there are any light sources in the room.
If there isn't it makes sure to add the dark cmdset to all
characters in the room. It is called whenever characters enter
the room and also by the Light sources when they turn on.
Args:
exclude (Object): An object to not include in the light check.
"""
if any(self._carries_light(obj) for obj in self.contents if obj != exclude):
self.locks.add("view:all()")
self.cmdset.remove(DarkCmdSet)
self.db.is_lit = True
for char in (obj for obj in self.contents if obj.has_account):
# this won't do anything if it is already removed
char.msg("The room is lit up.")
else:
# noone is carrying light - darken the room
self.db.is_lit = False
self.locks.add("view:false()")
self.cmdset.add(DarkCmdSet, permanent=True)
for char in (obj for obj in self.contents if obj.has_account):
if char.is_superuser:
char.msg("You are Superuser, so you are not affected by the dark state.")
else:
# put players in darkness
char.msg("The room is completely dark.")
[docs] def at_object_receive(self, obj, source_location):
"""
Called when an object enters the room.
"""
if obj.has_account:
# a puppeted object, that is, a Character
self._heal(obj)
# in case the new guy carries light with them
self.check_light_state()
[docs] def at_object_leave(self, obj, target_location):
"""
In case people leave with the light, we make sure to clear the
DarkCmdSet if necessary. This also works if they are
teleported away.
"""
# since this hook is called while the object is still in the room,
# we exclude it from the light check, to ignore any light sources
# it may be carrying.
self.check_light_state(exclude=obj)
# -------------------------------------------------------------
#
# Teleport room - puzzles solution
#
# This is a sort of puzzle room that requires a certain
# attribute on the entering character to be the same as
# an attribute of the room. If not, the character will
# be teleported away to a target location. This is used
# by the Obelisk - grave chamber puzzle, where one must
# have looked at the obelisk to get an attribute set on
# oneself, and then pick the grave chamber with the
# matching imagery for this attribute.
#
# -------------------------------------------------------------
[docs]class TeleportRoom(TutorialRoom):
"""
Teleporter - puzzle room.
Important attributes (set at creation):
puzzle_key - which attr to look for on character
puzzle_value - what char.db.puzzle_key must be set to
success_teleport_to - where to teleport in case if success
success_teleport_msg - message to echo while teleporting to success
failure_teleport_to - where to teleport to in case of failure
failure_teleport_msg - message to echo while teleporting to failure
"""
[docs] def at_object_creation(self):
"""Called at first creation"""
super().at_object_creation()
# what character.db.puzzle_clue must be set to, to avoid teleportation.
self.db.puzzle_value = 1
# target of successful teleportation. Can be a dbref or a
# unique room name.
self.db.success_teleport_msg = "You are successful!"
self.db.success_teleport_to = "treasure room"
# the target of the failure teleportation.
self.db.failure_teleport_msg = "You fail!"
self.db.failure_teleport_to = "dark cell"
[docs] def at_object_receive(self, character, source_location):
"""
This hook is called by the engine whenever the player is moved into
this room.
"""
if not character.has_account:
# only act on player characters.
return
# determine if the puzzle is a success or not
is_success = str(character.db.puzzle_clue) == str(self.db.puzzle_value)
teleport_to = self.db.success_teleport_to if is_success else self.db.failure_teleport_to
# note that this returns a list
results = search_object(teleport_to)
if not results or len(results) > 1:
# we cannot move anywhere since no valid target was found.
character.msg("no valid teleport target for %s was found." % teleport_to)
return
if character.is_superuser:
# superusers don't get teleported
character.msg("Superuser block: You would have been teleported to %s." % results[0])
return
# perform the teleport
if is_success:
character.msg(self.db.success_teleport_msg)
else:
character.msg(self.db.failure_teleport_msg)
# teleport quietly to the new place
character.move_to(results[0], quiet=True, move_hooks=False)
# we have to call this manually since we turn off move_hooks
# - this is necessary to make the target dark room aware of an
# already carried light.
results[0].at_object_receive(character, self)
# -------------------------------------------------------------
#
# Outro room - unique exit room
#
# Cleans up the character from all tutorial-related properties.
#
# -------------------------------------------------------------
[docs]class OutroRoom(TutorialRoom):
"""
Outro room.
Called when exiting the tutorial, cleans the
character of tutorial-related attributes.
"""
[docs] def at_object_creation(self):
"""
Called when the room is first created.
"""
super().at_object_creation()
self.db.tutorial_info = (
"The last room of the tutorial. "
"This cleans up all temporary Attributes "
"the tutorial may have assigned to the "
"character."
)
[docs] def at_object_receive(self, character, source_location):
"""
Do cleanup.
"""
if character.has_account:
del character.db.health_max
del character.db.health
del character.db.last_climbed
del character.db.puzzle_clue
del character.db.combat_parry_mode
del character.db.tutorial_bridge_position
for obj in character.contents:
if obj.typeclass_path.startswith("evennia.contrib.tutorial_world"):
obj.delete()
character.tags.clear(category="tutorial_world")
[docs] def at_object_leave(self, character, destination):
if character.account:
character.account.execute_cmd("unquell")