mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
1179 lines
42 KiB
Python
1179 lines
42 KiB
Python
"""
|
|
|
|
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, DefaultExit
|
|
from evennia import utils, create_object, search_object
|
|
from evennia import syscmdkeys, default_cmds
|
|
from .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
|
|
|
|
|
|
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"
|
|
|
|
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.
|
|
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"
|
|
|
|
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))
|
|
|
|
|
|
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"
|
|
|
|
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
|
|
|
|
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']
|
|
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
def at_cmdset_creation(self):
|
|
"""add the tutorial-room commands"""
|
|
self.add(CmdTutorial())
|
|
self.add(CmdTutorialSetDetail())
|
|
self.add(CmdTutorialLook())
|
|
self.add(CmdTutorialGiveUp())
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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}
|
|
|
|
|
|
class TutorialStartExit(DefaultExit):
|
|
"""
|
|
This is like a normal exit except it makes the `intro` command available
|
|
on itself. We put it on the exit in order to provide this command to the
|
|
Limbo room without modifying Limbo itself - deleting the tutorial exit
|
|
will also clean up the intro command.
|
|
|
|
"""
|
|
def at_object_creation(self):
|
|
self.cmdset.add(CmdSetEvenniaIntro, persistent=True)
|
|
|
|
|
|
# -------------------------------------------------------------
|
|
#
|
|
# 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.",
|
|
)
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
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."
|
|
|
|
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.
|
|
#
|
|
# -------------------------------------------------------------
|
|
|
|
class CmdEvenniaIntro(Command):
|
|
"""
|
|
Start the Evennia intro wizard.
|
|
|
|
Usage:
|
|
intro
|
|
|
|
"""
|
|
key = "intro"
|
|
|
|
def func(self):
|
|
from .intro_menu import init_menu
|
|
# quell also superusers
|
|
if self.caller.account:
|
|
self.caller.msg("Auto-quelling permissions while in intro ...")
|
|
self.caller.account.execute_cmd("quell")
|
|
init_menu(self.caller)
|
|
|
|
|
|
class CmdSetEvenniaIntro(CmdSet):
|
|
key = "Evennia Intro StartSet"
|
|
|
|
def at_cmdset_creation(self):
|
|
self.add(CmdEvenniaIntro())
|
|
|
|
|
|
class IntroRoom(TutorialRoom):
|
|
"""
|
|
Intro room
|
|
|
|
properties to customize:
|
|
char_health - integer > 0 (default 20)
|
|
"""
|
|
|
|
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."
|
|
)
|
|
|
|
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
|
|
#
|
|
# -------------------------------------------------------------
|
|
|
|
|
|
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"
|
|
|
|
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
|
|
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"
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
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"
|
|
|
|
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
|
|
class CmdBridgeHelp(Command):
|
|
"""
|
|
Overwritten help command while on the bridge.
|
|
"""
|
|
|
|
key = "help"
|
|
aliases = ["h", "?"]
|
|
locks = "cmd:all()"
|
|
help_category = "Tutorial world"
|
|
|
|
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)
|
|
|
|
|
|
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.
|
|
|
|
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.",
|
|
)
|
|
|
|
|
|
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).
|
|
|
|
"""
|
|
|
|
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, persistent=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()")
|
|
|
|
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))
|
|
|
|
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")
|
|
|
|
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."
|
|
)
|
|
|
|
|
|
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"
|
|
|
|
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)
|
|
|
|
|
|
class CmdDarkHelp(Command):
|
|
"""
|
|
Help command for the dark state.
|
|
"""
|
|
|
|
key = "help"
|
|
locks = "cmd:all()"
|
|
help_category = "TutorialWorld"
|
|
|
|
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)
|
|
|
|
|
|
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()"
|
|
|
|
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!"
|
|
)
|
|
|
|
|
|
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
|
|
|
|
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())
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
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, persistent=True)
|
|
|
|
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
|
|
|
|
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, persistent=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.")
|
|
|
|
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()
|
|
|
|
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.
|
|
#
|
|
# -------------------------------------------------------------
|
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
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"
|
|
|
|
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.
|
|
#
|
|
# -------------------------------------------------------------
|
|
|
|
|
|
class OutroRoom(TutorialRoom):
|
|
"""
|
|
Outro room.
|
|
|
|
Called when exiting the tutorial, cleans the
|
|
character of tutorial-related attributes.
|
|
|
|
"""
|
|
|
|
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."
|
|
)
|
|
|
|
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.tutorials.tutorial_world"):
|
|
obj.delete()
|
|
character.tags.clear(category="tutorial_world")
|
|
|
|
def at_object_leave(self, character, destination):
|
|
if character.account:
|
|
character.account.execute_cmd("unquell")
|
|
|