Start refactor contrib folder
This commit is contained in:
parent
7f0d314e7f
commit
f5f75bd04d
107 changed files with 34 additions and 2 deletions
4
evennia/contrib/tutorials/README.md
Normal file
4
evennia/contrib/tutorials/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Tutorial contribs
|
||||
|
||||
Resources specifically intended to help learn Evennia or particular concepts.
|
||||
Many of these accompany the official documentation.
|
||||
0
evennia/contrib/tutorials/tutorial_examples/__init__.py
Normal file
0
evennia/contrib/tutorials/tutorial_examples/__init__.py
Normal file
66
evennia/contrib/tutorials/tutorial_examples/bodyfunctions.py
Normal file
66
evennia/contrib/tutorials/tutorial_examples/bodyfunctions.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
62
evennia/contrib/tutorials/tutorial_examples/mirror.py
Normal file
62
evennia/contrib/tutorials/tutorial_examples/mirror.py
Normal 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.")
|
||||
577
evennia/contrib/tutorials/tutorial_examples/red_button.py
Normal file
577
evennia/contrib/tutorials/tutorial_examples/red_button.py
Normal 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)
|
||||
71
evennia/contrib/tutorials/tutorial_examples/tests.py
Normal file
71
evennia/contrib/tutorials/tutorial_examples/tests.py
Normal 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
|
||||
104
evennia/contrib/tutorials/tutorial_world/README.md
Normal file
104
evennia/contrib/tutorials/tutorial_world/README.md
Normal 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!
|
||||
7
evennia/contrib/tutorials/tutorial_world/__init__.py
Normal file
7
evennia/contrib/tutorials/tutorial_world/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This package holds the demo game of Evennia.
|
||||
"""
|
||||
|
||||
|
||||
from . import mob, objects, rooms
|
||||
1385
evennia/contrib/tutorials/tutorial_world/build.ev
Normal file
1385
evennia/contrib/tutorials/tutorial_world/build.ev
Normal file
File diff suppressed because it is too large
Load diff
782
evennia/contrib/tutorials/tutorial_world/intro_menu.py
Normal file
782
evennia/contrib/tutorials/tutorial_world/intro_menu.py
Normal 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)
|
||||
436
evennia/contrib/tutorials/tutorial_world/mob.py
Normal file
436
evennia/contrib/tutorials/tutorial_world/mob.py
Normal 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()
|
||||
1184
evennia/contrib/tutorials/tutorial_world/objects.py
Normal file
1184
evennia/contrib/tutorials/tutorial_world/objects.py
Normal file
File diff suppressed because it is too large
Load diff
1168
evennia/contrib/tutorials/tutorial_world/rooms.py
Normal file
1168
evennia/contrib/tutorials/tutorial_world/rooms.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue