Start refactor contrib folder

This commit is contained in:
Griatch 2021-12-18 11:32:34 +01:00
parent 7f0d314e7f
commit f5f75bd04d
107 changed files with 34 additions and 2 deletions

View file

@ -0,0 +1,4 @@
# Tutorial contribs
Resources specifically intended to help learn Evennia or particular concepts.
Many of these accompany the official documentation.

View file

@ -0,0 +1,66 @@
"""
Example script for testing. This adds a simple timer that has your
character make observations and notices at irregular intervals.
To test, use
@script me = tutorial_examples.bodyfunctions.BodyFunctions
The script will only send messages to the object it is stored on, so
make sure to put it on yourself or you won't see any messages!
"""
import random
from evennia import DefaultScript
class BodyFunctions(DefaultScript):
"""
This class defines the script itself
"""
def at_script_creation(self):
self.key = "bodyfunction"
self.desc = "Adds various timed events to a character."
self.interval = 20 # seconds
# self.repeats = 5 # repeat only a certain number of times
self.start_delay = True # wait self.interval until first call
# self.persistent = True
def at_repeat(self):
"""
This gets called every self.interval seconds. We make
a random check here so as to only return 33% of the time.
"""
if random.random() < 0.66:
# no message this time
return
self.send_random_message()
def send_random_message(self):
rand = random.random()
# return a random message
if rand < 0.1:
string = "You tap your foot, looking around."
elif rand < 0.2:
string = "You have an itch. Hard to reach too."
elif rand < 0.3:
string = (
"You think you hear someone behind you. ... but when you look there's noone there."
)
elif rand < 0.4:
string = "You inspect your fingernails. Nothing to report."
elif rand < 0.5:
string = "You cough discreetly into your hand."
elif rand < 0.6:
string = "You scratch your head, looking around."
elif rand < 0.7:
string = "You blink, forgetting what it was you were going to do."
elif rand < 0.8:
string = "You feel lonely all of a sudden."
elif rand < 0.9:
string = "You get a great idea. Of course you won't tell anyone."
else:
string = "You suddenly realize how much you love Evennia!"
# echo the message to the object
self.obj.msg(string)

View file

@ -0,0 +1,56 @@
#
# This is an example batch build file for Evennia.
#
# It allows batch processing of normal Evennia commands.
# Test it by loading it with @batchcommand:
#
# @batchcommand[/interactive] examples.batch_example
#
# A # as the first symbol on a line begins a comment and
# marks the end of a previous command definition (important!).
#
# All supplied commands are given as normal, on their own line
# and accept arguments in any format up until the first next
# comment line begins. Extra whitespace is removed; an empty
# line in a command definition translates into a newline.
#
# This creates a red button
@create button:tutorial_examples.red_button.RedButton
# This comment ends input for @create
# Next command:
@set button/desc =
This is a large red button. Now and then
it flashes in an evil, yet strangely tantalizing way.
A big sign sits next to it. It says:
-----------
Press me!
-----------
... It really begs to be pressed, doesn't it? You
know you want to!
# This ends the @set command. Note that line breaks and extra spaces
# in the argument are not considered. A completely empty line
# translates to a \n newline in the command; two empty lines will thus
# create a new paragraph. (note that few commands support it though, you
# mainly want to use it for descriptions).
# Now let's place the button where it belongs (let's say limbo #2 is
# the evil lair in our example).
@teleport #2
#... and drop it (remember, this comment ends input to @teleport, so don't
#forget it!) The very last command in the file need not be ended with #.
drop button

View file

@ -0,0 +1,90 @@
#
# Batchcode script
#
#
# The Batch-code processor accepts full Python modules (e.g. "batch.py") that
# look identical to normal Python files with a few exceptions that allows them
# to be executed in blocks. This way of working assures a sequential execution
# of the file and allows for features like stepping from block to block
# (without executing those coming before), as well as automatic deletion
# of created objects etc. You can however also run a batch-code python file
# directly using Python.
# Code blocks are separated by python comments starting with special code words.
# #HEADER - this denotes commands global to the entire file, such as
# import statements and global variables. They will
# automatically be made available for each block. Observe
# that changes to these variables made in one block are not
# preserved between blocks!)
# #CODE (infotext) [objname, objname, ...] - This designates a code block that
# will be executed like a stand-alone piece of code together with
# any #HEADER defined.
# infotext is a describing text about what goes on in this block.
# It will be shown by the batch-processing command.
# <objname>s mark the (variable-)names of objects created in
# the code, and which may be auto-deleted by the processor if
# desired (such as when debugging the script). E.g., if the code
# contains the command myobj = create.create_object(...), you could
# put 'myobj' in the #CODE header regardless of what the created
# object is actually called in-game.
# #INSERT filename - this includes another code batch file into this one. The
# named file will be loaded and run at the position of the #INSERT.
# Note that the inserted file will use its own #HEADERs and not
# have access to the #HEADERs of the inserting file.
# The following variable is automatically made available for the script:
# caller - the object executing the script
#
# HEADER
# everything in this block will be appended to the beginning of
# all other #CODE blocks when they are executed.
from evennia import create_object, search_object
from evennia.contrib.tutorial_examples import red_button
from evennia import DefaultObject
limbo = search_object("Limbo")[0]
# CODE
# This is the first code block. Within each block, Python
# code works as normal. Note how we make use if imports and
# 'limbo' defined in the #HEADER block. This block's header
# offers no information about red_button variable, so it
# won't be able to be deleted in debug mode.
# create a red button in limbo
red_button = create_object(
red_button.RedButton, key="Red button", location=limbo, aliases=["button"]
)
# we take a look at what we created
caller.msg("A %s was created." % red_button.key)
# CODE
# this code block has 'table' and 'chair' set as deletable
# objects. This means that when the batchcode processor runs in
# testing mode, objects created in these variables will be deleted
# again (so as to avoid duplicate objects when testing the script many
# times).
# the Python variables we assign to must match the ones given in the
# header for the system to be able to delete them afterwards during a
# debugging run.
table = create_object(DefaultObject, key="Table", location=limbo)
chair = create_object(DefaultObject, key="Chair", location=limbo)
string = "A %s and %s were created."
if DEBUG:
string += " Since debug was active, they were deleted again."
table.delete()
chair.delete()
caller.msg(string % (table, chair))

View file

@ -0,0 +1,62 @@
"""
TutorialMirror
A simple mirror object to experiment with.
"""
from evennia import DefaultObject
from evennia.utils import make_iter, is_iter
from evennia import logger
class TutorialMirror(DefaultObject):
"""
A simple mirror object that
- echoes back the description of the object looking at it
- echoes back whatever is being sent to its .msg - to the
sender, if given, otherwise to the location of the mirror.
"""
def return_appearance(self, looker, **kwargs):
"""
This formats the description of this object. Called by the 'look' command.
Args:
looker (Object): Object doing the looking.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
if isinstance(looker, self.__class__):
# avoid infinite recursion by having two mirrors look at each other
return "The image of yourself stretches into infinity."
return f"{self.key} shows your reflection:\n{looker.db.desc}"
def msg(self, text=None, from_obj=None, **kwargs):
"""
Simply override .msg to echo back to the messenger or to the current
location.
Args:
text (str or tuple, optional): The message to send. This
is treated internally like any send-command, so its
value can be a tuple if sending multiple arguments to
the `text` oob command.
from_obj (obj or iterable)
given, at_msg_send will be called. This value will be
passed on to the protocol. If iterable, will execute hook
on all entities in it.
"""
if not text:
text = "<silence>"
text = text[0] if is_iter(text) else text
if from_obj:
for obj in make_iter(from_obj):
obj.msg(f'{self.key} echoes back to you:\n"{text}".')
elif self.location:
self.location.msg_contents(f'{self.key} echoes back:\n"{text}".', exclude=[self])
else:
# no from_obj and no location, just log
logger.log_msg(f"{self.key}.msg was called without from_obj and .location is None.")

View file

@ -0,0 +1,577 @@
"""
This is a more advanced example object. It combines functions from
script.examples as well as commands.examples to make an interactive
button typeclass.
Create this button with
create/drop red_button.RedButton
Note that you must drop the button before you can see its messages!
## Technical
The button's functionality is controlled by CmdSets that gets added and removed
depending on the 'state' the button is in.
- Lid-closed state: In this state the button is covered by a glass cover and trying
to 'push' it will fail. You can 'nudge', 'smash' or 'open' the lid.
- Lid-open state: In this state the lid is open but will close again after a certain
time. Using 'push' now will press the button and trigger the Blind-state.
- Blind-state: In this mode you are blinded by a bright flash. This will affect your
normal commands like 'look' and help until the blindness wears off after a certain
time.
Timers are handled by persistent delays on the button. These are examples of
`evennia.utils.utils.delay` calls that wait a certain time before calling a method -
such as when closing the lid and un-blinding a character.
"""
import random
from evennia import DefaultObject
from evennia import Command, CmdSet
from evennia.utils.utils import delay, repeat, interactive
# Commands on the button (not all awailable at the same time)
# Commands for the state when the lid covering the button is closed.
class CmdPushLidClosed(Command):
"""
Push the red button (lid closed)
Usage:
push button
"""
key = "push button"
aliases = ["push", "press button", "press"]
locks = "cmd:all()"
def func(self):
"""
This is the version of push used when the lid is closed.
An alternative would be to make a 'push' command in a default cmdset
that is always available on the button and then use if-statements to
check if the lid is open or closed.
"""
self.caller.msg("You cannot push the button = there is a glass lid covering it.")
class CmdNudge(Command):
"""
Try to nudge the button's lid.
Usage:
nudge lid
This command will have you try to push the lid of the button away.
"""
key = "nudge lid" # two-word command name!
aliases = ["nudge"]
locks = "cmd:all()"
def func(self):
"""
Nudge the lid. Random chance of success to open it.
"""
rand = random.random()
if rand < 0.5:
self.caller.msg("You nudge at the lid. It seems stuck.")
elif rand < 0.7:
self.caller.msg("You move the lid back and forth. It won't budge.")
else:
self.caller.msg("You manage to get a nail under the lid.")
# self.obj is the button object
self.obj.to_open_state()
class CmdSmashGlass(Command):
"""
Smash the protective glass.
Usage:
smash glass
Try to smash the glass of the button.
"""
key = "smash glass"
aliases = ["smash lid", "break lid", "smash"]
locks = "cmd:all()"
def func(self):
"""
The lid won't open, but there is a small chance of causing the lamp to
break.
"""
rand = random.random()
self.caller.location.msg_contents(
f"{self.caller.name} tries to smash the glass of the button.",
exclude=self.caller)
if rand < 0.2:
string = ("You smash your hand against the glass"
" with all your might. The lid won't budge"
" but you cause quite the tremor through the button's mount."
"\nIt looks like the button's lamp stopped working for the time being, "
"but the lid is still as closed as ever.")
# self.obj is the button itself
self.obj.break_lamp()
elif rand < 0.6:
string = "You hit the lid hard. It doesn't move an inch."
else:
string = ("You place a well-aimed fist against the glass of the lid."
" Unfortunately all you get is a pain in your hand. Maybe"
" you should just try to just ... open the lid instead?")
self.caller.msg(string)
class CmdOpenLid(Command):
"""
open lid
Usage:
open lid
"""
key = "open lid"
aliases = ["open button"]
locks = "cmd:all()"
def func(self):
"simply call the right function."
if self.obj.db.lid_locked:
self.caller.msg("This lid seems locked in place for the moment.")
return
string = "\nA ticking sound is heard, like a winding mechanism. Seems "
string += "the lid will soon close again."
self.caller.msg(string)
self.caller.location.msg_contents(
f"{self.caller.name} opens the lid of the button.",
exclude=self.caller)
self.obj.to_open_state()
class LidClosedCmdSet(CmdSet):
"""
A simple cmdset tied to the redbutton object.
It contains the commands that launches the other
command sets, making the red button a self-contained
item (i.e. you don't have to manually add any
scripts etc to it when creating it).
Note that this is given with a `key_mergetype` set. This
is set up so that the cmdset with merge with Union merge type
*except* if the other cmdset to merge with is LidOpenCmdSet,
in which case it will Replace that. So these two cmdsets will
be mutually exclusive.
"""
key = "LidClosedCmdSet"
def at_cmdset_creation(self):
"Populates the cmdset when it is instantiated."
self.add(CmdPushLidClosed())
self.add(CmdNudge())
self.add(CmdSmashGlass())
self.add(CmdOpenLid())
# Commands for the state when the button's protective cover is open - now the
# push command will work. You can also close the lid again.
class CmdPushLidOpen(Command):
"""
Push the red button
Usage:
push button
"""
key = "push button"
aliases = ["push", "press button", "press"]
locks = "cmd:all()"
@interactive
def func(self):
"""
This version of push will immediately trigger the next button state.
The use of the @interactive decorator allows for using `yield` to add
simple pauses in how quickly a message is returned to the user. This
kind of pause will not survive a server reload.
"""
# pause a little between each message.
self.caller.msg("You reach out to press the big red button ...")
yield(2) # pause 2s before next message
self.caller.msg("\n\n|wBOOOOM! A bright light blinds you!|n")
yield(1) # pause 1s before next message
self.caller.msg("\n\n|xThe world goes dark ...|n")
name = self.caller.name
self.caller.location.msg_contents(
f"{name} presses the button. BOOM! {name} is blinded by a flash!",
exclude=self.caller)
self.obj.blind_target(self.caller)
class CmdCloseLid(Command):
"""
Close the lid
Usage:
close lid
Closes the lid of the red button.
"""
key = "close lid"
aliases = ["close"]
locks = "cmd:all()"
def func(self):
"Close the lid"
self.obj.to_closed_state()
# this will clean out scripts dependent on lid being open.
self.caller.msg("You close the button's lid. It clicks back into place.")
self.caller.location.msg_contents(
f"{self.caller.name} closes the button's lid.",
exclude=self.caller)
class LidOpenCmdSet(CmdSet):
"""
This is the opposite of the Closed cmdset.
Note that this is given with a `key_mergetype` set. This
is set up so that the cmdset with merge with Union merge type
*except* if the other cmdset to merge with is LidClosedCmdSet,
in which case it will Replace that. So these two cmdsets will
be mutually exclusive.
"""
key = "LidOpenCmdSet"
def at_cmdset_creation(self):
"""Setup the cmdset"""
self.add(CmdPushLidOpen())
self.add(CmdCloseLid())
# Commands for when the button has been pushed and the player is blinded. This
# replaces commands on the player making them 'blind' for a while.
class CmdBlindLook(Command):
"""
Looking around in darkness
Usage:
look <obj>
... not that there's much to see in the dark.
"""
key = "look"
aliases = ["l", "get", "examine", "ex", "feel", "listen"]
locks = "cmd:all()"
def func(self):
"This replaces all the senses when blinded."
# we decide what to reply based on which command was
# actually tried
if self.cmdstring == "get":
string = "You fumble around blindly without finding anything."
elif self.cmdstring == "examine":
string = "You try to examine your surroundings, but can't see a thing."
elif self.cmdstring == "listen":
string = "You are deafened by the boom."
elif self.cmdstring == "feel":
string = "You fumble around, hands outstretched. You bump your knee."
else:
# trying to look
string = ("You are temporarily blinded by the flash. "
"Until it wears off, all you can do is feel around blindly.")
self.caller.msg(string)
self.caller.location.msg_contents(
f"{self.caller.name} stumbles around, blinded.",
exclude=self.caller)
class CmdBlindHelp(Command):
"""
Help function while in the blinded state
Usage:
help
"""
key = "help"
aliases = "h"
locks = "cmd:all()"
def func(self):
"""
Just give a message while blinded. We could have added this to the
CmdBlindLook command too if we wanted to keep things more compact.
"""
self.caller.msg("You are beyond help ... until you can see again.")
class BlindCmdSet(CmdSet):
"""
This is the cmdset added to the *account* when
the button is pushed.
Since this has mergetype Replace it will completely remove the commands of
all other cmdsets while active. To allow some limited interaction
(pose/say) we import those default commands and add them too.
We also disable all exit-commands generated by exits and
object-interactions while blinded by setting `no_exits` and `no_objs` flags
on the cmdset. This is to avoid the player walking off or interfering with
other objects while blinded. Account-level commands however (channel messaging
etc) will not be affected by the blinding.
"""
key = "BlindCmdSet"
# we want it to completely replace all normal commands
# until the timed script removes it again.
mergetype = "Replace"
# we want to stop the player from walking around
# in this blinded state, so we hide all exits too.
# (channel commands will still work).
no_exits = True # keep player in the same room
no_objs = True # don't allow object commands
def at_cmdset_creation(self):
"Setup the blind cmdset"
from evennia.commands.default.general import CmdSay
from evennia.commands.default.general import CmdPose
self.add(CmdSay())
self.add(CmdPose())
self.add(CmdBlindLook())
self.add(CmdBlindHelp())
#
# Definition of the object itself
#
class RedButton(DefaultObject):
"""
This class describes an evil red button. It will blink invitingly and
temporarily blind whomever presses it.
The button can take a few optional attributes controlling how things will
be displayed in its various states. This is a useful way to give builders
the option to customize a complex object from in-game. Actual return messages
to event-actions are (in this example) left with each command, but one could
also imagine having those handled via Attributes as well, if one wanted a
completely in-game customizable button without needing to tweak command
classes.
Attributes:
- `desc_closed_lid`: This is the description to show of the button
when the lid is closed.
- `desc_open_lid`": Shown when the lid is open
- `auto_close_msg`: Message to show when lid auto-closes
- `desc_add_lamp_broken`: Extra desc-line added after normal desc when lamp
is broken.
- blink_msg: A list of strings to randomly choose from when the lamp
blinks.
Notes:
The button starts with lid closed. To set the initial description,
you can either set desc after creating it or pass a `desc` attribute
when creating it, such as
`button = create_object(RedButton, ..., attributes=[('desc', 'my desc')])`.
"""
# these are the pre-set descriptions. Setting attributes will override
# these on the fly.
desc_closed_lid = ("This is a large red button, inviting yet evil-looking. "
"A closed glass lid protects it.")
desc_open_lid = ("This is a large red button, inviting yet evil-looking. "
"Its glass cover is open and the button exposed.")
auto_close_msg = "The button's glass lid silently slides back in place."
lamp_breaks_msg = "The lamp flickers, the button going dark."
desc_add_lamp_broken = "\nThe big red button has stopped blinking for the time being."
# note that this is a list. A random message will display each time
blink_msgs = ["The red button flashes briefly.",
"The red button blinks invitingly.",
"The red button flashes. You know you wanna push it!"]
def at_object_creation(self):
"""
This function is called (once) when object is created.
"""
self.db.lamp_works = True
# start closed
self.to_closed_state()
# start blinking every 35s.
repeat(35, self._do_blink, persistent=True)
def _do_blink(self):
"""
Have the button blink invitingly unless it's broken.
"""
if self.location and self.db.lamp_works:
possible_messages = self.db.blink_msgs or self.blink_msgs
self.location.msg_contents(random.choice(possible_messages))
def _set_desc(self, attrname=None):
"""
Set a description, based on the attrname given, taking the lamp-status
into account.
Args:
attrname (str, optional): This will first check for an Attribute with this name,
secondly for a property on the class. So if `attrname="auto_close_msg"`,
we will first look for an attribute `.db.auto_close_msg` and if that's
not found we'll use `.auto_close_msg` instead. If unset (`None`), the
currently set desc will not be changed (only lamp will be checked).
Notes:
If `self.db.lamp_works` is `False`, we'll append
`desc_add_lamp_broken` text.
"""
if attrname:
# change desc
desc = self.attributes.get(attrname) or getattr(self, attrname)
else:
# use existing desc
desc = self.db.desc
if not self.db.lamp_works:
# lamp not working. Add extra to button's desc
desc += self.db.desc_add_lamp_broken or self.desc_add_lamp_broken
self.db.desc = desc
# state-changing methods and actions
def to_closed_state(self, msg=None):
"""
Switches the button to having its lid closed.
Args:
msg (str, optional): If given, display a message to the room
when lid closes.
This will first try to get the Attribute (self.db.desc_closed_lid) in
case it was set by a builder and if that was None, it will fall back to
self.desc_closed_lid, the default description (note that lack of .db).
"""
self._set_desc("desc_closed_lid")
# remove lidopen-state, if it exists
self.cmdset.remove(LidOpenCmdSet)
# add lid-closed cmdset
self.cmdset.add(LidClosedCmdSet)
if msg and self.location:
self.location.msg_contents(msg)
def to_open_state(self):
"""
Switches the button to having its lid open. This also starts a timer
that will eventually close it again.
"""
self._set_desc("desc_open_lid")
# remove lidopen-state, if it exists
self.cmdset.remove(LidClosedCmdSet)
# add lid-open cmdset
self.cmdset.add(LidOpenCmdSet)
# wait 20s then call self.to_closed_state with a message as argument
delay(35, self.to_closed_state,
self.db.auto_close_msg or self.auto_close_msg,
persistent=True)
def _unblind_target(self, caller):
"""
This is called to un-blind after a certain time.
"""
caller.cmdset.remove(BlindCmdSet)
caller.msg("You blink feverishly as your eyesight slowly returns.")
self.location.msg_contents(
f"{caller.name} seems to be recovering their eyesight, blinking feverishly.",
exclude=caller)
def blind_target(self, caller):
"""
Someone was foolish enough to press the button! Blind them
temporarily.
Args:
caller (Object): The one to be blinded.
"""
# we don't need to remove other cmdsets, this will replace all,
# then restore whatever was there when it goes away.
caller.cmdset.add(BlindCmdSet)
# wait 20s then call self._unblind to remove blindness effect. The
# persistent=True means the delay should survive a server reload.
delay(20, self._unblind_target, caller,
persistent=True)
def _unbreak_lamp(self):
"""
This is called to un-break the lamp after a certain time.
"""
# we do this quietly, the user will just notice it starting blinking again
self.db.lamp_works = True
self._set_desc()
def break_lamp(self):
"""
Breaks the lamp in the button, stopping it from blinking for a while
"""
self.db.lamp_works = False
# this will update the desc with the info about the broken lamp
self._set_desc()
self.location.msg_contents(self.db.lamp_breaks_msg or self.lamp_breaks_msg)
# wait 21s before unbreaking the lamp again
delay(21, self._unbreak_lamp)

View file

@ -0,0 +1,71 @@
from mock import Mock, patch
from evennia.utils.test_resources import EvenniaTest
from .bodyfunctions import BodyFunctions
@patch("evennia.contrib.tutorial_examples.bodyfunctions.random")
class TestBodyFunctions(EvenniaTest):
script_typeclass = BodyFunctions
def setUp(self):
super(TestBodyFunctions, self).setUp()
self.script.obj = self.char1
def tearDown(self):
super(TestBodyFunctions, self).tearDown()
# if we forget to stop the script, DirtyReactorAggregateError will be raised
self.script.stop()
def test_at_repeat(self, mock_random):
"""test that no message will be sent when below the 66% threshold"""
mock_random.random = Mock(return_value=0.5)
old_func = self.script.send_random_message
self.script.send_random_message = Mock()
self.script.at_repeat()
self.script.send_random_message.assert_not_called()
# test that random message will be sent
mock_random.random = Mock(return_value=0.7)
self.script.at_repeat()
self.script.send_random_message.assert_called()
self.script.send_random_message = old_func
def test_send_random_message(self, mock_random):
"""Test that correct message is sent for each random value"""
old_func = self.char1.msg
self.char1.msg = Mock()
# test each of the values
mock_random.random = Mock(return_value=0.05)
self.script.send_random_message()
self.char1.msg.assert_called_with("You tap your foot, looking around.")
mock_random.random = Mock(return_value=0.15)
self.script.send_random_message()
self.char1.msg.assert_called_with("You have an itch. Hard to reach too.")
mock_random.random = Mock(return_value=0.25)
self.script.send_random_message()
self.char1.msg.assert_called_with(
"You think you hear someone behind you. ... " "but when you look there's noone there."
)
mock_random.random = Mock(return_value=0.35)
self.script.send_random_message()
self.char1.msg.assert_called_with("You inspect your fingernails. Nothing to report.")
mock_random.random = Mock(return_value=0.45)
self.script.send_random_message()
self.char1.msg.assert_called_with("You cough discreetly into your hand.")
mock_random.random = Mock(return_value=0.55)
self.script.send_random_message()
self.char1.msg.assert_called_with("You scratch your head, looking around.")
mock_random.random = Mock(return_value=0.65)
self.script.send_random_message()
self.char1.msg.assert_called_with("You blink, forgetting what it was you were going to do.")
mock_random.random = Mock(return_value=0.75)
self.script.send_random_message()
self.char1.msg.assert_called_with("You feel lonely all of a sudden.")
mock_random.random = Mock(return_value=0.85)
self.script.send_random_message()
self.char1.msg.assert_called_with("You get a great idea. Of course you won't tell anyone.")
mock_random.random = Mock(return_value=0.95)
self.script.send_random_message()
self.char1.msg.assert_called_with("You suddenly realize how much you love Evennia!")
self.char1.msg = old_func

View file

@ -0,0 +1,104 @@
# Evennia Tutorial World
Griatch 2011, 2015
This is a stand-alone tutorial area for an unmodified Evennia install.
Think of it as a sort of single-player adventure rather than a
full-fledged multi-player game world. The various rooms and objects
herein are designed to show off features of the engine, not to be a
very challenging (nor long) gaming experience. As such it's of course
only skimming the surface of what is possible.
## Install
Log in as superuser (#1), then run
@batchcommand tutorial_world.build
Wait a little while for building to complete and don't run the command
again. This should build the world and connect it to Limbo.
If you are a superuser (User `#1`), use the `@quell` command to play
the tutorial as intended.
## Comments
The tutorial world is intended for your playing around with the engine.
It will help you learn how to accomplish some more advanced effects
and might give some good ideas along the way.
It's suggested you play it through (as a normal user, NOT as
Superuser!) and explore it a bit, then come back here and start
looking into the (heavily documented) build/source code to find out
how things tick - that's the "tutorial" in Tutorial world after all.
Please report bugs in the tutorial to the Evennia issue tracker.
**Spoilers below - don't read on unless you already played the
tutorial game**
## Tutorial World Room map
?
|
+---+----+ +-------------------+ +--------+ +--------+
| | | | |gate | |corner |
| cliff +----+ bridge +----+ +---+ |
| | | | | | | |
+---+---\+ +---------------+---+ +---+----+ +---+----+
| \ | | castle |
| \ +--------+ +----+---+ +---+----+ +---+----+
| \ |under- | |ledge | |along | |court- |
| \|ground +--+ | |wall +---+yard |
| \ | | | | | | |
| +------\-+ +--------+ +--------+ +---+----+
| \ |
++---------+ \ +--------+ +--------+ +---+----+
|intro | \ |cell | |trap | |temple |
o--+ | \| +----+ | | |
L | | \ | /| | | |
I +----+-----+ +--------+ / ---+-+-+-+ +---+----+
M | / | | | |
B +----+-----+ +--------+/ +--+-+-+---------+----+
O |outro | |tomb | |antechamber |
o--+ +----------+ | | |
| | | | | |
+----------+ +--------+ +---------------------+
## Hints/Notes:
* o-- connections to/from Limbo
* intro/outro areas are rooms that automatically sets/cleans the
Character of any settings assigned to it during the
tutorial game.
* The Cliff is a good place to get an overview of the surroundings.
* The Bridge may seem like a big room, but it is really only one room
with custom move commands to make it take longer to cross. You can
also fall off the bridge if you are unlucky or take your time to
take in the view too long.
* In the Castle areas an aggressive mob is patrolling. It implements
rudimentary AI but packs quite a punch unless you have
found yourself a weapon that can harm it. Combat is only
possible once you find a weapon.
* The Antechamber features a puzzle for finding the correct Grave
chamber.
* The Cell is your reward if you fail in various ways. Finding a
way out of it is a small puzzle of its own.
* The Tomb is a nice place to find a weapon that can hurt the
castle guardian. This is the goal of the tutorial.
Explore on, or take the exit to finish the tutorial.
* ? - look into the code if you cannot find this bonus area!

View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
This package holds the demo game of Evennia.
"""
from . import mob, objects, rooms

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,782 @@
"""
Intro menu / game tutor
Evennia contrib - Griatch 2020
This contrib is an intro-menu for general MUD and evennia usage using the
EvMenu menu-templating system.
EvMenu templating is a way to create a menu using a string-format instead
of creating all nodes manually. Of course, for full functionality one must
still create the goto-callbacks.
"""
from evennia import create_object
from evennia import CmdSet
from evennia.utils.evmenu import parse_menu_template, EvMenu
# Goto callbacks and helper resources for the menu
def do_nothing(caller, raw_string, **kwargs):
"""
Re-runs the current node
"""
return None
def send_testing_tagged(caller, raw_string, **kwargs):
"""
Test to send a message to a pane tagged with 'testing' in the webclient.
"""
caller.msg(
(
"This is a message tagged with 'testing' and "
"should appear in the pane you selected!\n "
f"You wrote: '{raw_string}'",
{"type": "testing"},
)
)
return None
# Resources for the first help-command demo
class DemoCommandSetHelp(CmdSet):
"""
Demo the help command
"""
key = "Help Demo Set"
priority = 2
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
def goto_command_demo_help(caller, raw_string, **kwargs):
"Sets things up before going to the help-demo node"
_maintain_demo_room(caller, delete=True)
caller.cmdset.remove(DemoCommandSetRoom)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.add(DemoCommandSetHelp) # TODO - make persistent
return kwargs.get("gotonode") or "command_demo_help"
# Resources for the comms demo
class DemoCommandSetComms(CmdSet):
"""
Demo communications
"""
key = "Color Demo Set"
priority = 2
no_exits = True
no_objs = True
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdPose())
self.add(default_cmds.CmdPage())
self.add(default_cmds.CmdColorTest())
def goto_command_demo_comms(caller, raw_string, **kwargs):
"""
Setup and go to the color demo node.
"""
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetRoom)
caller.cmdset.add(DemoCommandSetComms)
return kwargs.get("gotonode") or "comms_demo_start"
# Resources for the room demo
_ROOM_DESC = """
This is a small and comfortable wood cabin. Bright sunlight is shining in
through the windows.
Use |ylook sign|n or |yl sign|n to examine the wooden sign nailed to the wall.
"""
_SIGN_DESC = """
The small sign reads:
Good! Now try '|ylook small|n'.
... You'll get a multi-match error! There are two things that 'small' could
refer to here - the 'small wooden sign' or the 'small, cozy cabin' itself. You will
get a list of the possibilities.
You could either tell Evennia which one you wanted by picking a unique part
of their name (like '|ylook cozy|n') or use the number in the list to pick
the one you want, like this:
|ylook 2-small|n
As long as what you write is uniquely identifying you can be lazy and not
write the full name of the thing you want to look at. Try '|ylook bo|n',
'|yl co|n' or '|yl 1-sm|n'!
... Oh, and if you see database-ids like (#1245) by the name of objects,
it's because you are playing with Builder-privileges or higher. Regular
players will not see the numbers.
Next try |ylook door|n.
"""
_DOOR_DESC_OUT = """
This is a solid wooden door leading to the outside of the cabin. Some
text is written on it:
This is an |wexit|n. An exit is often named by its compass-direction like
|weast|n, |wwest|n, |wnorthwest|n and so on, but it could be named
anything, like this door. To use the exit, you just write its name. So by
writing |ydoor|n you will leave the cabin.
"""
_DOOR_DESC_IN = """
This is a solid wooden door leading to the inside of the cabin. On
are some carved text:
This exit leads back into the cabin. An exit is just like any object,
so while has a name, it can also have aliases. To get back inside
you can both write |ydoor|n but also |yin|n.
"""
_MEADOW_DESC = """
This is a lush meadow, just outside a cozy cabin. It's surrounded
by trees and sunlight filters down from a clear blue sky.
There is a |wstone|n here. Try looking at it!
"""
_STONE_DESC = """
This is a fist-sized stone covered in runes:
To pick me up, use
|yget stone|n
You can see what you carry with the |yinventory|n (|yi|n).
To drop me again, just write
|ydrop stone|n
Use |ynext|n when you are done exploring and want to
continue with the tutorial.
"""
def _maintain_demo_room(caller, delete=False):
"""
Handle the creation/cleanup of demo assets. We store them
on the character and clean them when leaving the menu later.
"""
# this is a tuple (room, obj)
roomdata = caller.db.tutorial_world_demo_room_data
if delete:
if roomdata:
# we delete directly for simplicity. We need to delete
# in specific order to avoid deleting rooms moves
# its contents to their default home-location
prev_loc, room1, sign, room2, stone, door_out, door_in = roomdata
caller.location = prev_loc
sign.delete()
stone.delete()
door_out.delete()
door_in.delete()
room1.delete()
room2.delete()
del caller.db.tutorial_world_demo_room_data
elif not roomdata:
# create and describe the cabin and box
room1 = create_object("evennia.objects.objects.DefaultRoom", key="A small, cozy cabin")
room1.db.desc = _ROOM_DESC.lstrip()
sign = create_object(
"evennia.objects.objects.DefaultObject", key="small wooden sign", location=room1
)
sign.db.desc = _SIGN_DESC.strip()
sign.locks.add("get:false()")
sign.db.get_err_msg = "The sign is nailed to the wall. It's not budging."
# create and describe the meadow and stone
room2 = create_object("evennia.objects.objects.DefaultRoom", key="A lush summer meadow")
room2.db.desc = _MEADOW_DESC.lstrip()
stone = create_object(
"evennia.objects.objects.DefaultObject", key="carved stone", location=room2
)
stone.db.desc = _STONE_DESC.strip()
# make the linking exits
door_out = create_object(
"evennia.objects.objects.DefaultExit",
key="Door",
location=room1,
destination=room2,
locks=["get:false()"],
)
door_out.db.desc = _DOOR_DESC_OUT.strip()
door_in = create_object(
"evennia.objects.objects.DefaultExit",
key="entrance to the cabin",
aliases=["door", "in", "entrance"],
location=room2,
destination=room1,
locks=["get:false()"],
)
door_in.db.desc = _DOOR_DESC_IN.strip()
# store references for easy removal later
caller.db.tutorial_world_demo_room_data = (
caller.location,
room1,
sign,
room2,
stone,
door_out,
door_in,
)
# move caller into room
caller.location = room1
class DemoCommandSetRoom(CmdSet):
"""
Demo some general in-game commands command.
"""
key = "Room Demo Set"
priority = 2
no_exits = False
no_objs = False
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
self.add(default_cmds.CmdLook())
self.add(default_cmds.CmdGet())
self.add(default_cmds.CmdDrop())
self.add(default_cmds.CmdInventory())
self.add(default_cmds.CmdExamine())
self.add(default_cmds.CmdPy())
def goto_command_demo_room(caller, raw_string, **kwargs):
"""
Setup and go to the demo-room node. Generates a little 2-room environment
for testing out some commands.
"""
_maintain_demo_room(caller)
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.add(DemoCommandSetRoom)
return "command_demo_room"
def goto_cleanup_cmdsets(caller, raw_strings, **kwargs):
"""
Cleanup all cmdsets.
"""
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.remove(DemoCommandSetRoom)
return kwargs.get("gotonode")
# register all callables that can be used in the menu template
GOTO_CALLABLES = {
"send_testing_tagged": send_testing_tagged,
"do_nothing": do_nothing,
"goto_command_demo_help": goto_command_demo_help,
"goto_command_demo_comms": goto_command_demo_comms,
"goto_command_demo_room": goto_command_demo_room,
"goto_cleanup_cmdsets": goto_cleanup_cmdsets,
}
# Main menu definition
MENU_TEMPLATE = """
## NODE start
|g** Evennia introduction wizard **|n
If you feel lost you can learn some of the basics of how to play a text-based
game here. You can also learn a little about the system and how to find more
help. You can exit this tutorial-wizard at any time by entering '|yq|n' or '|yquit|n'.
Press |y<return>|n or write |ynext|n to step forward. Or select a number to jump to.
## OPTIONS
1 (next);1;next;n: What is a MUD/MU*? -> about_muds
2: About Evennia -> about_evennia
3: Using the webclient -> using webclient
4: The help command -> goto_command_demo_help()
5: Communicating with others -> goto_command_demo_help(gotonode='talk on channels')
6: Using colors -> goto_command_demo_comms(gotonode='testing_colors')
7: Moving and exploring -> goto_command_demo_room()
8: Conclusions & next steps-> conclusions
>: about_muds
# ---------------------------------------------------------------------------------
## NODE about_muds
|g** About MUDs **|n
The term '|wMUD|n' stands for Multi-user-Dungeon or -Dimension. A MUD is
primarily played by inserting text |wcommands|n and getting text back.
MUDS were the |wprecursors|n to graphical MMORPG-style games like World of
Warcraft. While not as mainstream as they once were, comparing a text-game to a
graphical game is like comparing a book to a movie - it's just a different
experience altogether.
MUDs are |wdifferent|n from Interactive Fiction (IF) in that they are multiplayer
and usually has a consistent game world with many stories and protagonists
acting at the same time.
Like there are many different styles of graphical MMOs, there are |wmany
variations|n of MUDs: They can be slow-paced or fast. They can cover fantasy,
sci-fi, horror or other genres. They can allow PvP or not and be casual or
hardcore, strategic, tactical, turn-based or play in real-time.
Whereas 'MUD' is arguably the most well-known term, there are other terms
centered around particular game engines - such as MUSH, MOO, MUX, MUCK, LPMuds,
ROMs, Diku and others. Many people that played MUDs in the past used one of
these existing families of text game-servers, whether they knew it or not.
|cEvennia|n is a newer text game engine designed to emulate almost any existing
gaming style you like and possibly any new ones you can come up with!
## OPTIONS
next;n: About Evennia -> about_evennia
back to start;back;start;t: start
>: about_evennia
# ---------------------------------------------------------------------------------
## NODE about_evennia
|g** About Evennia **|n
|cEvennia|n is a Python game engine for creating multiplayer online text-games
(aka MUDs, MUSHes, MUX, MOOs...). It is open-source and |wfree to use|n, also for
commercial projects (BSD license).
Out of the box, Evennia provides a |wworking, if empty game|n. Whereas you can play
via traditional telnet MUD-clients, the server runs your game's website and
offers a |wHTML5 webclient|n so that people can play your game in their browser
without downloading anything extra.
Evennia deliberately |wdoes not|n hard-code any game-specific things like
combat-systems, races, skills, etc. They would not match what just you wanted
anyway! Whereas we do have optional contribs with many examples, most of our
users use them as inspiration to make their own thing.
Evennia is developed entirely in |wPython|n, using modern developer practices.
The advantage of text is that even a solo developer or small team can
realistically make a competitive multiplayer game (as compared to a graphical
MMORPG which is one of the most expensive game types in existence to develop).
Many also use Evennia as a |wfun way to learn Python|n!
## OPTIONS
next;n: Using the webclient -> using webclient
back;b: About MUDs -> about_muds
>: using webclient
# ---------------------------------------------------------------------------------
## NODE using webclient
|g** Using the Webclient **|n
|RNote: This is only relevant if you use Evennia's HTML5 web client. If you use a
third-party (telnet) mud-client, you can skip this section.|n
Evennia's web client is (for a local install) found by pointing your browser to
|yhttp://localhost:4001/webclient|n
For a live example, the public Evennia demo can be found at
|yhttps://demo.evennia.com/webclient|n
The web client starts out having two panes - the input-pane for entering commands
and the main window.
- Use |y<Return>|n (or click the arrow on the right) to send your input.
- Use |yCtrl + <up/down-arrow>|n to step back and forth in your command-history.
- Use |yCtrl + <Return>|n to add a new line to your input without sending.
(Cmd instead of Ctrl-key on Macs)
There is also some |wextra|n info to learn about customizing the webclient.
## OPTIONS
extra: Customizing the webclient -> customizing the webclient
next;n: Playing the game -> goto_command_demo_help()
back;b: About Evennia -> about_evennia
back to start;start: start
>: goto_command_demo_help()
# ---------------------------------------------------------------------------------
# this is a dead-end 'leaf' of the menu
## NODE customizing the webclient
|g** Extra hints on customizing the Webclient **|n
|y1)|n The panes of the webclient can be resized and you can create additional panes.
- Press the little plus (|w+|n) sign in the top left and a new tab will appear.
- Click and drag the tab and pull it far to the right and release when it creates two
panes next to each other.
|y2)|n You can have certain server output only appear in certain panes.
- In your new rightmost pane, click the diamond () symbol at the top.
- Unselect everything and make sure to select "testing".
- Click the diamond again so the menu closes.
- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane!
|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper
left corner. It allows to change things like font and if the client should play sound.
The "message routing" allows for rerouting text matching a certain regular expression (regex)
to a web client pane with a specific tag that you set yourself.
|y4)|n Close the right-hand pane with the |wX|n in the rop right corner.
## OPTIONS
back;b: using webclient
> test *: send tagged message to new pane -> send_testing_tagged()
>: using webclient
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_help()
## NODE command_demo_help
|g** Playing the game **|n
Evennia has about |w90 default commands|n. They include useful administration/building
commands and a few limited "in-game" commands to serve as examples. They are intended
to be changed, extended and modified as you please.
First to try is |yhelp|n. This lists all commands |wcurrently|n available to you.
Use |yhelp <topic>|n to get specific help. Try |yhelp help|n to get help on using
the help command. For your game you could add help about your game, lore, rules etc
as well.
At the moment you only have |whelp|n and some |wChannel Names|n (the '<menu commands>'
is just a placeholder to indicate you are using this menu).
We'll add more commands as we get to them in this tutorial - but we'll only
cover a small handful. Once you exit you'll find a lot more! Now let's try
those channels ...
## OPTIONS
next;n: Talk on Channels -> talk on channels
back;b: Using the webclient -> goto_cleanup_cmdsets(gotonode='using webclient')
back to start;start: start
>: talk on channels
# ---------------------------------------------------------------------------------
## NODE talk on channels
|g** Talk on Channels **|n
|wChannels|n are like in-game chatrooms. The |wChannel Names|n help-category
holds the names of the channels available to you right now. One such channel is
|wpublic|n. Use |yhelp public|n to see how to use it. Try it:
|ypublic Hello World!|n
This will send a message to the |wpublic|n channel where everyone on that
channel can see it. If someone else is on your server, you may get a reply!
Evennia can link its in-game channels to external chat networks. This allows
you to talk with people not actually logged into the game. For
example, the online Evennia-demo links its |wpublic|n channel to the #evennia
IRC support channel.
## OPTIONS
next;n: Talk to people in-game -> goto_command_demo_comms()
back;b: Finding help -> goto_command_demo_help()
back to start;start: start
>: goto_command_demo_comms()
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_comms()
## NODE comms_demo_start
|g** Talk to people in-game **|n
You can also chat with people inside the game. If you try |yhelp|n now you'll
find you have a few more commands available for trying this out.
|ysay Hello there!|n
|y'Hello there!|n
|wsay|n is used to talk to people in the same location you are. Everyone in the
room will see what you have to say. A single quote |y'|n is a convenient shortcut.
|ypose smiles|n
|y:smiles|n
|wpose|n (or |wemote|n) describes what you do to those nearby. This is a very simple
command by default, but it can be extended to much more complex parsing in order to
include other people/objects in the emote, reference things by a short-description etc.
## OPTIONS
next;n: Paging people -> paging_people
back;b: Talk on Channels -> goto_command_demo_help(gotonode='talk on channels')
back to start;start: start
>: paging_people
# ---------------------------------------------------------------------------------
## NODE paging_people
|g** Paging people **|n
Halfway between talking on a |wChannel|n and chatting in your current location
with |wsay|n and |wpose|n, you can also |wpage|n people. This is like a private
message only they can see.
|ypage <name> = Hello there!
page <name1>, <name2> = Hello both of you!|n
If you are alone on the server, put your own name as |w<name>|n to test it and
page yourself. Write just |ypage|n to see your latest pages. This will also show
you if anyone paged you while you were offline.
(By the way - depending on which games you are used to, you may think that the
use of |y=|n above is strange. This is a MUSH/MUX-style of syntax. For your own
game you can change the |wpose|n command to work however you prefer).
## OPTIONS
next;n: Using colors -> testing_colors
back;b: Talk to people in-game -> comms_demo_start
back to start;start: start
>: testing_colors
# ---------------------------------------------------------------------------------
## NODE testing_colors
|g** U|rs|yi|gn|wg |c|yc|wo|rl|bo|gr|cs |g**|n
You can add color in your text by the help of tags. However, remember that not
everyone will see your colors - it depends on their client (and some use
screenreaders). Using color can also make text harder to read. So use it
sparingly.
To start coloring something |rred|n, add a ||r (red) marker and then
end with ||n (to go back to neutral/no-color):
|ysay This is a ||rred||n text!
say This is a ||Rdark red||n text!|n
You can also change the background:
|ysay This is a ||[x||bblue text on a light-grey background!|n
There are 16 base colors and as many background colors (called ANSI colors). Some
clients also supports so-called Xterm256 which gives a total of 256 colors. These are
given as |w||rgb|n, where r, g, b are the components of red, green and blue from 0-5:
|ysay This is ||050solid green!|n
|ysay This is ||520an orange color!|n
|ysay This is ||[005||555white on bright blue background!|n
If you don't see the expected colors from the above examples, it's because your
client does not support it - try out the Evennia webclient instead. To see all
color codes printed, try
|ycolor ansi
|ycolor xterm
## OPTIONS
next;n: Moving and Exploring -> goto_command_demo_room()
back;b: Paging people -> goto_command_demo_comms(gotonode='paging_people')
back to start;start: start
>: goto_command_demo_room()
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_room()
## NODE command_demo_room
|gMoving and Exploring|n
For exploring the game, a very important command is '|ylook|n'. It's also
abbreviated '|yl|n' since it's used so much. Looking displays/redisplays your
current location. You can also use it to look closer at items in the world. So
far in this tutorial, using 'look' would just redisplay the menu.
Try |ylook|n now. You have been quietly transported to a sunny cabin to look
around in. Explore a little and use |ynext|n when you are done.
## OPTIONS
next;n: Conclusions -> conclusions
back;b: Channel commands -> goto_command_demo_comms(gotonode='testing_colors')
back to start;start: start
>: conclusions
# ---------------------------------------------------------------------------------
## NODE conclusions
|gConclusions|n
That concludes this little quick-intro to using the base game commands of
Evennia. With this you should be able to continue exploring and also find help
if you get stuck!
Write |ynext|n to end this wizard and continue to the tutorial-world quest!
If you want there is also some |wextra|n info for where to go beyond that.
## OPTIONS
extra: Where to go next -> post scriptum
next;next;n: End -> end
back;b: Moving and Exploring -> goto_command_demo_room()
back to start;start: start
>: end
# ---------------------------------------------------------------------------------
## NODE post scriptum
|gWhere to next?|n
After playing through the tutorial-world quest, if you aim to make a game with
Evennia you are wise to take a look at the |wEvennia documentation|n at
|yhttps://www.evennia.com/docs/latest|n
- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
|yhttps://www.evennia.com/docs/latest/Building-Quickstart|n
- The tutorial-world may or may not be your cup of tea, but it does show off
several |wuseful tools|n of Evennia. You may want to check out how it works:
|yhttps://www.evennia.com/docs/latest/Tutorial-World-Introduction|n
- You can then continue looking through the |wTutorials|n and pick one that
fits your level of understanding.
|yhttps://www.evennia.com/docs/latest/Tutorials|n
- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The
Evennia community is very active and friendly and no question is too simple.
You will often quickly get help. You can everything you need linked from
|yhttps://www.evennia.com|n
# ---------------------------------------------------------------------------------
## OPTIONS
back: conclusions
>: conclusions
## NODE end
|gGood luck!|n
"""
# -------------------------------------------------------------------------------------------
#
# EvMenu implementation and access function
#
# -------------------------------------------------------------------------------------------
class TutorialEvMenu(EvMenu):
"""
Custom EvMenu for displaying the intro-menu
"""
def close_menu(self):
"""Custom cleanup actions when closing menu"""
self.caller.cmdset.remove(DemoCommandSetHelp)
self.caller.cmdset.remove(DemoCommandSetRoom)
self.caller.cmdset.remove(DemoCommandSetComms)
_maintain_demo_room(self.caller, delete=True)
super().close_menu()
def options_formatter(self, optionslist):
navigation_keys = ("next", "back", "back to start")
other = []
navigation = []
for key, desc in optionslist:
if key in navigation_keys:
desc = f" ({desc})" if desc else ""
navigation.append(f"|lc{key}|lt|w{key}|n|le{desc}")
else:
other.append((key, desc))
navigation = (
(" " + " |W|||n ".join(navigation) + " |W|||n " + "|wQ|Wuit|n") if navigation else ""
)
other = super().options_formatter(other)
sep = "\n\n" if navigation and other else ""
return f"{navigation}{sep}{other}"
def init_menu(caller):
"""
Call to initialize the menu.
"""
menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES)
TutorialEvMenu(caller, menutree)

View file

@ -0,0 +1,436 @@
"""
This module implements a simple mobile object with
a very rudimentary AI as well as an aggressive enemy
object based on that mobile class.
"""
import random
from evennia import TICKER_HANDLER
from evennia import search_object
from evennia import Command, CmdSet
from evennia import logger
from evennia.contrib.tutorial_world import objects as tut_objects
class CmdMobOnOff(Command):
"""
Activates/deactivates Mob
Usage:
mobon <mob>
moboff <mob>
This turns the mob from active (alive) mode
to inactive (dead) mode. It is used during
building to activate the mob once it's
prepared.
"""
key = "mobon"
aliases = "moboff"
locks = "cmd:superuser()"
def func(self):
"""
Uses the mob's set_alive/set_dead methods
to turn on/off the mob."
"""
if not self.args:
self.caller.msg("Usage: mobon||moboff <mob>")
return
mob = self.caller.search(self.args)
if not mob:
return
if self.cmdstring == "mobon":
mob.set_alive()
else:
mob.set_dead()
class MobCmdSet(CmdSet):
"""
Holds the admin command controlling the mob
"""
def at_cmdset_creation(self):
self.add(CmdMobOnOff())
class Mob(tut_objects.TutorialObject):
"""
This is a state-machine AI mobile. It has several states which are
controlled from setting various Attributes. All default to True:
patrolling: if set, the mob will move randomly
from room to room, but preferring to not return
the way it came. If unset, the mob will remain
stationary (idling) until attacked.
aggressive: if set, will attack Characters in
the same room using whatever Weapon it
carries (see tutorial_world.objects.TutorialWeapon).
if unset, the mob will never engage in combat
no matter what.
hunting: if set, the mob will pursue enemies trying
to flee from it, so it can enter combat. If unset,
it will return to patrolling/idling if fled from.
immortal: If set, the mob cannot take any damage.
irregular_echoes: list of strings the mob generates at irregular intervals.
desc_alive: the physical description while alive
desc_dead: the physical descripion while dead
send_defeated_to: unique key/alias for location to send defeated enemies to
defeat_msg: message to echo to defeated opponent
defeat_msg_room: message to echo to room. Accepts %s as the name of the defeated.
hit_msg: message to echo when this mob is hit. Accepts %s for the mob's key.
weapon_ineffective_msg: message to echo for useless attacks
death_msg: message to echo to room when this mob dies.
patrolling_pace: how many seconds per tick, when patrolling
aggressive_pace: -"- attacking
hunting_pace: -"- hunting
death_pace: -"- returning to life when dead
field 'home' - the home location should set to someplace inside
the patrolling area. The mob will use this if it should
happen to roam into a room with no exits.
"""
def at_init(self):
"""
When initialized from cache (after a server reboot), set up
the AI state.
"""
# The AI state machine (not persistent).
self.ndb.is_patrolling = self.db.patrolling and not self.db.is_dead
self.ndb.is_attacking = False
self.ndb.is_hunting = False
self.ndb.is_immortal = self.db.immortal or self.db.is_dead
def at_object_creation(self):
"""
Called the first time the object is created.
We set up the base properties and flags here.
"""
self.cmdset.add(MobCmdSet, persistent=True)
# Main AI flags. We start in dead mode so we don't have to
# chase the mob around when building.
self.db.patrolling = True
self.db.aggressive = True
self.db.immortal = False
# db-store if it is dead or not
self.db.is_dead = True
# specifies how much damage we divide away from non-magic weapons
self.db.damage_resistance = 100.0
# pace (number of seconds between ticks) for
# the respective modes.
self.db.patrolling_pace = 6
self.db.aggressive_pace = 2
self.db.hunting_pace = 1
self.db.death_pace = 100 # stay dead for 100 seconds
# we store the call to the tickerhandler
# so we can easily deactivate the last
# ticker subscription when we switch.
# since we will use the same idstring
# throughout we only need to save the
# previous interval we used.
self.db.last_ticker_interval = None
# store two separate descriptions, one for alive and
# one for dead (corpse)
self.db.desc_alive = "This is a moving object."
self.db.desc_dead = "A dead body."
# health stats
self.db.full_health = 20
self.db.health = 20
# when this mob defeats someone, we move the character off to
# some other place (Dark Cell in the tutorial).
self.db.send_defeated_to = "dark cell"
# text to echo to the defeated foe.
self.db.defeat_msg = "You fall to the ground."
self.db.defeat_msg_room = "%s falls to the ground."
self.db.weapon_ineffective_msg = (
"Your weapon just passes through your enemy, causing almost no effect!"
)
self.db.death_msg = "After the last hit %s evaporates." % self.key
self.db.hit_msg = "%s wails, shudders and writhes." % self.key
self.db.irregular_msgs = ["the enemy looks about.", "the enemy changes stance."]
self.db.tutorial_info = "This is an object with simple state AI, using a ticker to move."
def _set_ticker(self, interval, hook_key, stop=False):
"""
Set how often the given hook key should
be "ticked".
Args:
interval (int or None): The number of seconds
between ticks
hook_key (str or None): The name of the method
(on this mob) to call every interval
seconds.
stop (bool, optional): Just stop the
last ticker without starting a new one.
With this set, the interval and hook_key
arguments are unused.
In order to only have one ticker
running at a time, we make sure to store the
previous ticker subscription so that we can
easily find and stop it before setting a
new one. The tickerhandler is persistent so
we need to remember this across reloads.
"""
idstring = "tutorial_mob" # this doesn't change
last_interval = self.db.last_ticker_interval
last_hook_key = self.db.last_hook_key
if last_interval and last_hook_key:
# we have a previous subscription, kill this first.
TICKER_HANDLER.remove(
interval=last_interval, callback=getattr(self, last_hook_key), idstring=idstring
)
self.db.last_ticker_interval = interval
self.db.last_hook_key = hook_key
if not stop:
# set the new ticker
TICKER_HANDLER.add(
interval=interval, callback=getattr(self, hook_key), idstring=idstring
)
def _find_target(self, location):
"""
Scan the given location for suitable targets (this is defined
as Characters) to attack. Will ignore superusers.
Args:
location (Object): the room to scan.
Returns:
The first suitable target found.
"""
targets = [
obj
for obj in location.contents_get(exclude=self)
if obj.has_account and not obj.is_superuser
]
return targets[0] if targets else None
def set_alive(self, *args, **kwargs):
"""
Set the mob to "alive" mode. This effectively
resurrects it from the dead state.
"""
self.db.health = self.db.full_health
self.db.is_dead = False
self.db.desc = self.db.desc_alive
self.ndb.is_immortal = self.db.immortal
self.ndb.is_patrolling = self.db.patrolling
if not self.location:
self.move_to(self.home)
if self.db.patrolling:
self.start_patrolling()
def set_dead(self):
"""
Set the mob to "dead" mode. This turns it off
and makes sure it can take no more damage.
It also starts a ticker for when it will return.
"""
self.db.is_dead = True
self.location = None
self.ndb.is_patrolling = False
self.ndb.is_attacking = False
self.ndb.is_hunting = False
self.ndb.is_immortal = True
# we shall return after some time
self._set_ticker(self.db.death_pace, "set_alive")
def start_idle(self):
"""
Starts just standing around. This will kill
the ticker and do nothing more.
"""
self._set_ticker(None, None, stop=True)
def start_patrolling(self):
"""
Start the patrolling state by
registering us with the ticker-handler
at a leasurely pace.
"""
if not self.db.patrolling:
self.start_idle()
return
self._set_ticker(self.db.patrolling_pace, "do_patrol")
self.ndb.is_patrolling = True
self.ndb.is_hunting = False
self.ndb.is_attacking = False
# for the tutorial, we also heal the mob in this mode
self.db.health = self.db.full_health
def start_hunting(self):
"""
Start the hunting state
"""
if not self.db.hunting:
self.start_patrolling()
return
self._set_ticker(self.db.hunting_pace, "do_hunt")
self.ndb.is_patrolling = False
self.ndb.is_hunting = True
self.ndb.is_attacking = False
def start_attacking(self):
"""
Start the attacking state
"""
if not self.db.aggressive:
self.start_hunting()
return
self._set_ticker(self.db.aggressive_pace, "do_attack")
self.ndb.is_patrolling = False
self.ndb.is_hunting = False
self.ndb.is_attacking = True
def do_patrol(self, *args, **kwargs):
"""
Called repeatedly during patrolling mode. In this mode, the
mob scans its surroundings and randomly chooses a viable exit.
One should lock exits with the traverse:has_account() lock in
order to block the mob from moving outside its area while
allowing account-controlled characters to move normally.
"""
if random.random() < 0.01 and self.db.irregular_msgs:
self.location.msg_contents(random.choice(self.db.irregular_msgs))
if self.db.aggressive:
# first check if there are any targets in the room.
target = self._find_target(self.location)
if target:
self.start_attacking()
return
# no target found, look for an exit.
exits = [exi for exi in self.location.exits if exi.access(self, "traverse")]
if exits:
# randomly pick an exit
exit = random.choice(exits)
# move there.
self.move_to(exit.destination)
else:
# no exits! teleport to home to get away.
self.move_to(self.home)
def do_hunting(self, *args, **kwargs):
"""
Called regularly when in hunting mode. In hunting mode the mob
scans adjacent rooms for enemies and moves towards them to
attack if possible.
"""
if random.random() < 0.01 and self.db.irregular_msgs:
self.location.msg_contents(random.choice(self.db.irregular_msgs))
if self.db.aggressive:
# first check if there are any targets in the room.
target = self._find_target(self.location)
if target:
self.start_attacking()
return
# no targets found, scan surrounding rooms
exits = [exi for exi in self.location.exits if exi.access(self, "traverse")]
if exits:
# scan the exits destination for targets
for exit in exits:
target = self._find_target(exit.destination)
if target:
# a target found. Move there.
self.move_to(exit.destination)
return
# if we get to this point we lost our
# prey. Resume patrolling.
self.start_patrolling()
else:
# no exits! teleport to home to get away.
self.move_to(self.home)
def do_attack(self, *args, **kwargs):
"""
Called regularly when in attacking mode. In attacking mode
the mob will bring its weapons to bear on any targets
in the room.
"""
if random.random() < 0.01 and self.db.irregular_msgs:
self.location.msg_contents(random.choice(self.db.irregular_msgs))
# first make sure we have a target
target = self._find_target(self.location)
if not target:
# no target, start looking for one
self.start_hunting()
return
# we use the same attack commands as defined in
# tutorial_world.objects.TutorialWeapon, assuming that
# the mob is given a Weapon to attack with.
attack_cmd = random.choice(("thrust", "pierce", "stab", "slash", "chop"))
self.execute_cmd("%s %s" % (attack_cmd, target))
# analyze the current state
if target.db.health <= 0:
# we reduced the target to <= 0 health. Move them to the
# defeated room
target.msg(self.db.defeat_msg)
self.location.msg_contents(self.db.defeat_msg_room % target.key, exclude=target)
send_defeated_to = search_object(self.db.send_defeated_to)
if send_defeated_to:
target.move_to(send_defeated_to[0], quiet=True)
else:
logger.log_err(
"Mob: mob.db.send_defeated_to not found: %s" % self.db.send_defeated_to
)
# response methods - called by other objects
def at_hit(self, weapon, attacker, damage):
"""
Someone landed a hit on us. Check our status
and start attacking if not already doing so.
"""
if self.db.health is None:
# health not set - this can't be damaged.
attacker.msg(self.db.weapon_ineffective_msg)
return
if not self.ndb.is_immortal:
if not weapon.db.magic:
# not a magic weapon - divide away magic resistance
damage /= self.db.damage_resistance
attacker.msg(self.db.weapon_ineffective_msg)
else:
self.location.msg_contents(self.db.hit_msg)
self.db.health -= damage
# analyze the result
if self.db.health <= 0:
# we are dead!
attacker.msg(self.db.death_msg)
self.set_dead()
else:
# still alive, start attack if not already attacking
if self.db.aggressive and not self.ndb.is_attacking:
self.start_attacking()
def at_new_arrival(self, new_character):
"""
This is triggered whenever a new character enters the room.
This is called by the TutorialRoom the mob stands in and
allows it to be aware of changes immediately without needing
to poll for them all the time. For example, the mob can react
right away, also when patrolling on a very slow ticker.
"""
# the room actually already checked all we need, so
# we know it is a valid target.
if self.db.aggressive and not self.ndb.is_attacking:
self.start_attacking()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff