Resolve merge conflicts
This commit is contained in:
commit
76555e6ca5
34 changed files with 4332 additions and 412 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
# Sept 2017:
|
# Sept 2017:
|
||||||
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
|
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
|
||||||
'Account', rework the website template and a slew of other updates.
|
'Account', rework the website template and a slew of other updates.
|
||||||
Info on what changed and how to migrat is found here:
|
Info on what changed and how to migrate is found here:
|
||||||
https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ
|
https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ
|
||||||
|
|
||||||
## Feb 2017:
|
## Feb 2017:
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ RUN apk update && apk add python py-pip python-dev py-setuptools gcc musl-dev jp
|
||||||
ADD . /usr/src/evennia
|
ADD . /usr/src/evennia
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
RUN pip install -e /usr/src/evennia --trusted-host pypi.python.org
|
RUN pip install --upgrade pip && pip install /usr/src/evennia --trusted-host pypi.python.org
|
||||||
|
|
||||||
# add the game source when rebuilding a new docker image from inside
|
# add the game source when rebuilding a new docker image from inside
|
||||||
# a game dir
|
# a game dir
|
||||||
|
|
@ -48,7 +48,7 @@ WORKDIR /usr/src/game
|
||||||
ENV PS1 "evennia|docker \w $ "
|
ENV PS1 "evennia|docker \w $ "
|
||||||
|
|
||||||
# startup a shell when we start the container
|
# startup a shell when we start the container
|
||||||
ENTRYPOINT ["bash"]
|
ENTRYPOINT bash -c "source /usr/src/evennia/bin/unix/evennia-docker-start.sh"
|
||||||
|
|
||||||
# expose the telnet, webserver and websocket client ports
|
# expose the telnet, webserver and websocket client ports
|
||||||
EXPOSE 4000 4001 4005
|
EXPOSE 4000 4001 4005
|
||||||
|
|
|
||||||
13
bin/unix/evennia-docker-start.sh
Normal file
13
bin/unix/evennia-docker-start.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
# called by the Dockerfile to start the server in docker mode
|
||||||
|
|
||||||
|
# remove leftover .pid files (such as from when dropping the container)
|
||||||
|
rm /usr/src/game/server/*.pid >& /dev/null || true
|
||||||
|
|
||||||
|
# start evennia server; log to server.log but also output to stdout so it can
|
||||||
|
# be viewed with docker-compose logs
|
||||||
|
exec 3>&1; evennia start 2>&1 1>&3 | tee /usr/src/game/server/logs/server.log; exec 3>&-
|
||||||
|
|
||||||
|
# start a shell to keep the container running
|
||||||
|
bash
|
||||||
|
|
@ -761,7 +761,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
elif _MULTISESSION_MODE in (2, 3):
|
elif _MULTISESSION_MODE in (2, 3):
|
||||||
# In this mode we by default end up at a character selection
|
# In this mode we by default end up at a character selection
|
||||||
# screen. We execute look on the account.
|
# screen. We execute look on the account.
|
||||||
# we make sure to clean up the _playable_characers list in case
|
# we make sure to clean up the _playable_characters list in case
|
||||||
# any was deleted in the interim.
|
# any was deleted in the interim.
|
||||||
self.db._playable_characters = [char for char in self.db._playable_characters if char]
|
self.db._playable_characters = [char for char in self.db._playable_characters if char]
|
||||||
self.msg(self.at_look(target=self.db._playable_characters,
|
self.msg(self.at_look(target=self.db._playable_characters,
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
||||||
if desc:
|
if desc:
|
||||||
new_character.db.desc = desc
|
new_character.db.desc = desc
|
||||||
elif not new_character.db.desc:
|
elif not new_character.db.desc:
|
||||||
new_character.db.desc = "This is an Account."
|
new_character.db.desc = "This is a character."
|
||||||
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
|
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
|
||||||
% (new_character.key, new_character.key))
|
% (new_character.key, new_character.key))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1455,6 +1455,13 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
|
|
||||||
Switch:
|
Switch:
|
||||||
edit: Open the line editor (string values only)
|
edit: Open the line editor (string values only)
|
||||||
|
script: If we're trying to set an attribute on a script
|
||||||
|
channel: If we're trying to set an attribute on a channel
|
||||||
|
account: If we're trying to set an attribute on an account
|
||||||
|
room: Setting an attribute on a room (global search)
|
||||||
|
exit: Setting an attribute on an exit (global search)
|
||||||
|
char: Setting an attribute on a character (global search)
|
||||||
|
character: Alias for char, as above.
|
||||||
|
|
||||||
Sets attributes on objects. The second form clears
|
Sets attributes on objects. The second form clears
|
||||||
a previously set attribute while the last form
|
a previously set attribute while the last form
|
||||||
|
|
@ -1555,6 +1562,38 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
# start the editor
|
# start the editor
|
||||||
EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr))
|
EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr))
|
||||||
|
|
||||||
|
def search_for_obj(self, objname):
|
||||||
|
"""
|
||||||
|
Searches for an object matching objname. The object may be of different typeclasses.
|
||||||
|
Args:
|
||||||
|
objname: Name of the object we're looking for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A typeclassed object, or None if nothing is found.
|
||||||
|
"""
|
||||||
|
from evennia.utils.utils import variable_from_module
|
||||||
|
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||||
|
caller = self.caller
|
||||||
|
if objname.startswith('*') or "account" in self.switches:
|
||||||
|
found_obj = caller.search_account(objname.lstrip('*'))
|
||||||
|
elif "script" in self.switches:
|
||||||
|
found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller)
|
||||||
|
elif "channel" in self.switches:
|
||||||
|
found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller)
|
||||||
|
else:
|
||||||
|
global_search = True
|
||||||
|
if "char" in self.switches or "character" in self.switches:
|
||||||
|
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||||
|
elif "room" in self.switches:
|
||||||
|
typeclass = settings.BASE_ROOM_TYPECLASS
|
||||||
|
elif "exit" in self.switches:
|
||||||
|
typeclass = settings.BASE_EXIT_TYPECLASS
|
||||||
|
else:
|
||||||
|
global_search = False
|
||||||
|
typeclass = None
|
||||||
|
found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass)
|
||||||
|
return found_obj
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Implement the set attribute - a limited form of @py."""
|
"""Implement the set attribute - a limited form of @py."""
|
||||||
|
|
||||||
|
|
@ -1568,10 +1607,7 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
objname = self.lhs_objattr[0]['name']
|
objname = self.lhs_objattr[0]['name']
|
||||||
attrs = self.lhs_objattr[0]['attrs']
|
attrs = self.lhs_objattr[0]['attrs']
|
||||||
|
|
||||||
if objname.startswith('*'):
|
obj = self.search_for_obj(objname)
|
||||||
obj = caller.search_account(objname.lstrip('*'))
|
|
||||||
else:
|
|
||||||
obj = caller.search(objname)
|
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -267,13 +267,17 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg("You can't get that.")
|
caller.msg("You can't get that.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# calling at_before_get hook method
|
||||||
|
if not obj.at_before_get(caller):
|
||||||
|
return
|
||||||
|
|
||||||
obj.move_to(caller, quiet=True)
|
obj.move_to(caller, quiet=True)
|
||||||
caller.msg("You pick up %s." % obj.name)
|
caller.msg("You pick up %s." % obj.name)
|
||||||
caller.location.msg_contents("%s picks up %s." %
|
caller.location.msg_contents("%s picks up %s." %
|
||||||
(caller.name,
|
(caller.name,
|
||||||
obj.name),
|
obj.name),
|
||||||
exclude=caller)
|
exclude=caller)
|
||||||
# calling hook method
|
# calling at_get hook method
|
||||||
obj.at_get(caller)
|
obj.at_get(caller)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -308,6 +312,10 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Call the object script's at_before_drop() method.
|
||||||
|
if not obj.at_before_drop(caller):
|
||||||
|
return
|
||||||
|
|
||||||
obj.move_to(caller.location, quiet=True)
|
obj.move_to(caller.location, quiet=True)
|
||||||
caller.msg("You drop %s." % (obj.name,))
|
caller.msg("You drop %s." % (obj.name,))
|
||||||
caller.location.msg_contents("%s drops %s." %
|
caller.location.msg_contents("%s drops %s." %
|
||||||
|
|
@ -350,6 +358,11 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
||||||
if not to_give.location == caller:
|
if not to_give.location == caller:
|
||||||
caller.msg("You are not holding %s." % to_give.key)
|
caller.msg("You are not holding %s." % to_give.key)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# calling at_before_give hook method
|
||||||
|
if not to_give.at_before_give(caller, target):
|
||||||
|
return
|
||||||
|
|
||||||
# give object
|
# give object
|
||||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||||
to_give.move_to(target, quiet=True)
|
to_give.move_to(target, quiet=True)
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
return
|
return
|
||||||
if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)):
|
if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)):
|
||||||
string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @/./+/-/_/' only." \
|
string = "\n\r Password should be longer than 3 characters. Letters, spaces, digits and @/./+/-/_/' only." \
|
||||||
"\nFor best security, make it longer than 8 characters. You can also use a phrase of" \
|
"\nFor best security, make it longer than 8 characters. You can also use a phrase of" \
|
||||||
"\nmany words if you enclose the password in double quotes."
|
"\nmany words if you enclose the password in double quotes."
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
|
|
@ -557,7 +557,7 @@ def _create_character(session, new_account, typeclass, home, permissions):
|
||||||
|
|
||||||
# If no description is set, set a default description
|
# If no description is set, set a default description
|
||||||
if not new_character.db.desc:
|
if not new_character.db.desc:
|
||||||
new_character.db.desc = "This is an Account."
|
new_character.db.desc = "This is a character."
|
||||||
# We need to set this to have @ic auto-connect to this character
|
# We need to set this to have @ic auto-connect to this character
|
||||||
new_account.db._last_puppet = new_character
|
new_account.db._last_puppet = new_character
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ things you want from here into your game folder and change them there.
|
||||||
for any game. Allows safe trading of any godds (including coin)
|
for any game. Allows safe trading of any godds (including coin)
|
||||||
* CharGen (Griatch 2011) - A simple Character creator for OOC mode.
|
* CharGen (Griatch 2011) - A simple Character creator for OOC mode.
|
||||||
Meant as a starting point for a more fleshed-out system.
|
Meant as a starting point for a more fleshed-out system.
|
||||||
* Clothing (BattleJenkins 2017) - A layered clothing system with
|
* Clothing (FlutterSprite 2017) - A layered clothing system with
|
||||||
slots for different types of garments auto-showing in description.
|
slots for different types of garments auto-showing in description.
|
||||||
* Color-markups (Griatch, 2017) - Alternative in-game color markups.
|
* Color-markups (Griatch, 2017) - Alternative in-game color markups.
|
||||||
* Custom gametime (Griatch, vlgeoff 2017) - Implements Evennia's
|
* Custom gametime (Griatch, vlgeoff 2017) - Implements Evennia's
|
||||||
|
|
@ -50,8 +50,9 @@ things you want from here into your game folder and change them there.
|
||||||
time to pass depending on if you are walking/running etc.
|
time to pass depending on if you are walking/running etc.
|
||||||
* Talking NPC (Griatch 2011) - A talking NPC object that offers a
|
* Talking NPC (Griatch 2011) - A talking NPC object that offers a
|
||||||
menu-driven conversation tree.
|
menu-driven conversation tree.
|
||||||
* Turnbattle (BattleJenkins 2017) - A turn-based combat engine meant
|
* Tree Select (FlutterSprite 2017) - A simple system for creating a
|
||||||
as a start to build from. Has attack/disengage and turn timeouts.
|
branching EvMenu with selection options sourced from a single
|
||||||
|
multi-line string.
|
||||||
* Wilderness (titeuf87 2017) - Make infinitely large wilderness areas
|
* Wilderness (titeuf87 2017) - Make infinitely large wilderness areas
|
||||||
with dynamically created locations.
|
with dynamically created locations.
|
||||||
* UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax.
|
* UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax.
|
||||||
|
|
@ -62,6 +63,9 @@ things you want from here into your game folder and change them there.
|
||||||
to the Evennia game index (games.evennia.com)
|
to the Evennia game index (games.evennia.com)
|
||||||
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
|
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
|
||||||
objects and events using Python from in-game.
|
objects and events using Python from in-game.
|
||||||
|
* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant
|
||||||
|
as a start to build from. Has attack/disengage and turn timeouts,
|
||||||
|
and includes optional expansions for equipment and combat movement.
|
||||||
* Tutorial examples (Griatch 2011, 2015) - A folder of basic
|
* Tutorial examples (Griatch 2011, 2015) - A folder of basic
|
||||||
example objects, commands and scripts.
|
example objects, commands and scripts.
|
||||||
* Tutorial world (Griatch 2011, 2015) - A folder containing the
|
* Tutorial world (Griatch 2011, 2015) - A folder containing the
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ class CmdUnconnectedCreate(MuxCommand):
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
return
|
return
|
||||||
if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)):
|
if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)):
|
||||||
string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @/./+/-/_/' only." \
|
string = "\n\r Password should be longer than 3 characters. Letters, spaces, digits and @/./+/-/_/' only." \
|
||||||
"\nFor best security, make it longer than 8 characters. You can also use a phrase of" \
|
"\nFor best security, make it longer than 8 characters. You can also use a phrase of" \
|
||||||
"\nmany words if you enclose the password in double quotes."
|
"\nmany words if you enclose the password in double quotes."
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
|
|
|
||||||
103
evennia/contrib/health_bar.py
Normal file
103
evennia/contrib/health_bar.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
"""
|
||||||
|
Health Bar
|
||||||
|
|
||||||
|
Contrib - Tim Ashley Jenkins 2017
|
||||||
|
|
||||||
|
The function provided in this module lets you easily display visual
|
||||||
|
bars or meters - "health bar" is merely the most obvious use for this,
|
||||||
|
though these bars are highly customizable and can be used for any sort
|
||||||
|
of appropriate data besides player health.
|
||||||
|
|
||||||
|
Today's players may be more used to seeing statistics like health,
|
||||||
|
stamina, magic, and etc. displayed as bars rather than bare numerical
|
||||||
|
values, so using this module to present this data this way may make it
|
||||||
|
more accessible. Keep in mind, however, that players may also be using
|
||||||
|
a screen reader to connect to your game, which will not be able to
|
||||||
|
represent the colors of the bar in any way. By default, the values
|
||||||
|
represented are rendered as text inside the bar which can be read by
|
||||||
|
screen readers.
|
||||||
|
|
||||||
|
The health bar will account for current values above the maximum or
|
||||||
|
below 0, rendering them as a completely full or empty bar with the
|
||||||
|
values displayed within.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def display_meter(cur_value, max_value,
|
||||||
|
length=30, fill_color=["R", "Y", "G"],
|
||||||
|
empty_color="B", text_color="w",
|
||||||
|
align="left", pre_text="", post_text="",
|
||||||
|
show_values=True):
|
||||||
|
"""
|
||||||
|
Represents a current and maximum value given as a "bar" rendered with
|
||||||
|
ANSI or xterm256 background colors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cur_value (int): Current value to display
|
||||||
|
max_value (int): Maximum value to display
|
||||||
|
|
||||||
|
Options:
|
||||||
|
length (int): Length of meter returned, in characters
|
||||||
|
fill_color (list): List of color codes for the full portion
|
||||||
|
of the bar, sans any sort of prefix - both ANSI and xterm256
|
||||||
|
colors are usable. When the bar is empty, colors toward the
|
||||||
|
start of the list will be chosen - when the bar is full, colors
|
||||||
|
towards the end are picked. You can adjust the 'weights' of
|
||||||
|
the changing colors by adding multiple entries of the same
|
||||||
|
color - for example, if you only want the bar to change when
|
||||||
|
it's close to empty, you could supply ['R','Y','G','G','G']
|
||||||
|
empty_color (str): Color code for the empty portion of the bar.
|
||||||
|
text_color (str): Color code for text inside the bar.
|
||||||
|
align (str): "left", "right", or "center" - alignment of text in the bar
|
||||||
|
pre_text (str): Text to put before the numbers in the bar
|
||||||
|
post_text (str): Text to put after the numbers in the bar
|
||||||
|
show_values (bool): If true, shows the numerical values represented by
|
||||||
|
the bar. It's highly recommended you keep this on, especially if
|
||||||
|
there's no info given in pre_text or post_text, as players on screen
|
||||||
|
readers will be unable to read the graphical aspect of the bar.
|
||||||
|
"""
|
||||||
|
# Start by building the base string.
|
||||||
|
num_text = ""
|
||||||
|
if show_values:
|
||||||
|
num_text = "%i / %i" % (cur_value, max_value)
|
||||||
|
bar_base_str = pre_text + num_text + post_text
|
||||||
|
# Cut down the length of the base string if needed
|
||||||
|
if len(bar_base_str) > length:
|
||||||
|
bar_base_str = bar_base_str[:length]
|
||||||
|
# Pad and align the bar base string
|
||||||
|
if align == "right":
|
||||||
|
bar_base_str = bar_base_str.rjust(length, " ")
|
||||||
|
elif align == "center":
|
||||||
|
bar_base_str = bar_base_str.center(length, " ")
|
||||||
|
else:
|
||||||
|
bar_base_str = bar_base_str.ljust(length, " ")
|
||||||
|
|
||||||
|
if max_value < 1: # Prevent divide by zero
|
||||||
|
max_value = 1
|
||||||
|
if cur_value < 0: # Prevent weirdly formatted 'negative bars'
|
||||||
|
cur_value = 0
|
||||||
|
if cur_value > max_value: # Display overfull bars correctly
|
||||||
|
cur_value = max_value
|
||||||
|
|
||||||
|
# Now it's time to determine where to put the color codes.
|
||||||
|
percent_full = float(cur_value) / float(max_value)
|
||||||
|
split_index = round(float(length) * percent_full)
|
||||||
|
# Determine point at which to split the bar
|
||||||
|
split_index = int(split_index)
|
||||||
|
|
||||||
|
# Separate the bar string into full and empty portions
|
||||||
|
full_portion = bar_base_str[:split_index]
|
||||||
|
empty_portion = bar_base_str[split_index:]
|
||||||
|
|
||||||
|
# Pick which fill color to use based on how full the bar is
|
||||||
|
fillcolor_index = (float(len(fill_color)) * percent_full)
|
||||||
|
fillcolor_index = int(round(fillcolor_index)) - 1
|
||||||
|
fillcolor_code = "|[" + fill_color[fillcolor_index]
|
||||||
|
|
||||||
|
# Make color codes for empty bar portion and text_color
|
||||||
|
emptycolor_code = "|[" + empty_color
|
||||||
|
textcolor_code = "|" + text_color
|
||||||
|
|
||||||
|
# Assemble the final bar
|
||||||
|
final_bar = fillcolor_code + textcolor_code + full_portion + "|n" + emptycolor_code + textcolor_code + empty_portion + "|n"
|
||||||
|
|
||||||
|
return final_bar
|
||||||
|
|
@ -671,6 +671,15 @@ class TestGenderSub(CommandTest):
|
||||||
txt = "Test |p gender"
|
txt = "Test |p gender"
|
||||||
self.assertEqual(gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender")
|
self.assertEqual(gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender")
|
||||||
|
|
||||||
|
# test health bar contrib
|
||||||
|
|
||||||
|
from evennia.contrib import health_bar
|
||||||
|
|
||||||
|
class TestHealthBar(EvenniaTest):
|
||||||
|
def test_healthbar(self):
|
||||||
|
expected_bar_str = "|[G|w |n|[B|w test24 / 200test |n"
|
||||||
|
self.assertTrue(health_bar.display_meter(24, 200, length=40, pre_text="test", post_text="test", align="center") == expected_bar_str)
|
||||||
|
|
||||||
# test mail contrib
|
# test mail contrib
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -926,7 +935,7 @@ class TestTutorialWorldRooms(CommandTest):
|
||||||
|
|
||||||
|
|
||||||
# test turnbattle
|
# test turnbattle
|
||||||
from evennia.contrib import turnbattle
|
from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range
|
||||||
from evennia.objects.objects import DefaultRoom
|
from evennia.objects.objects import DefaultRoom
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -934,60 +943,94 @@ class TestTurnBattleCmd(CommandTest):
|
||||||
|
|
||||||
# Test combat commands
|
# Test combat commands
|
||||||
def test_turnbattlecmd(self):
|
def test_turnbattlecmd(self):
|
||||||
self.call(turnbattle.CmdFight(), "", "You can't start a fight if you've been defeated!")
|
self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!")
|
||||||
self.call(turnbattle.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
|
self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
|
||||||
self.call(turnbattle.CmdPass(), "", "You can only do that in combat. (see: help fight)")
|
self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)")
|
||||||
self.call(turnbattle.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
|
self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
|
||||||
self.call(turnbattle.CmdRest(), "", "Char rests to recover HP.")
|
self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.")
|
||||||
|
|
||||||
|
# Test equipment commands
|
||||||
|
def test_turnbattleequipcmd(self):
|
||||||
|
# Start with equip module specific commands.
|
||||||
|
testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
|
||||||
|
testarmor = create_object(tb_equip.TBEArmor, key="test armor")
|
||||||
|
testweapon.move_to(self.char1)
|
||||||
|
testarmor.move_to(self.char1)
|
||||||
|
self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.")
|
||||||
|
self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.")
|
||||||
|
self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.")
|
||||||
|
self.call(tb_equip.CmdDoff(), "", "Char removes test armor.")
|
||||||
|
# Also test the commands that are the same in the basic module
|
||||||
|
self.call(tb_equip.CmdFight(), "", "You can't start a fight if you've been defeated!")
|
||||||
|
self.call(tb_equip.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.")
|
||||||
|
|
||||||
|
# Test range commands
|
||||||
|
def test_turnbattlerangecmd(self):
|
||||||
|
# Start with range module specific commands.
|
||||||
|
self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100")
|
||||||
|
# Also test the commands that are the same in the basic module
|
||||||
|
self.call(tb_range.CmdFight(), "", "There's nobody here to fight!")
|
||||||
|
self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
|
||||||
|
self.call(tb_range.CmdRest(), "", "Char rests to recover HP.")
|
||||||
|
|
||||||
|
|
||||||
class TestTurnBattleFunc(EvenniaTest):
|
class TestTurnBattleFunc(EvenniaTest):
|
||||||
|
|
||||||
# Test combat functions
|
# Test combat functions
|
||||||
def test_turnbattlefunc(self):
|
def test_tbbasicfunc(self):
|
||||||
attacker = create_object(turnbattle.BattleCharacter, key="Attacker")
|
attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker")
|
||||||
defender = create_object(turnbattle.BattleCharacter, key="Defender")
|
defender = create_object(tb_basic.TBBasicCharacter, key="Defender")
|
||||||
testroom = create_object(DefaultRoom, key="Test Room")
|
testroom = create_object(DefaultRoom, key="Test Room")
|
||||||
attacker.location = testroom
|
attacker.location = testroom
|
||||||
defender.loaction = testroom
|
defender.loaction = testroom
|
||||||
# Initiative roll
|
# Initiative roll
|
||||||
initiative = turnbattle.roll_init(attacker)
|
initiative = tb_basic.roll_init(attacker)
|
||||||
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||||
# Attack roll
|
# Attack roll
|
||||||
attack_roll = turnbattle.get_attack(attacker, defender)
|
attack_roll = tb_basic.get_attack(attacker, defender)
|
||||||
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
|
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
|
||||||
# Defense roll
|
# Defense roll
|
||||||
defense_roll = turnbattle.get_defense(attacker, defender)
|
defense_roll = tb_basic.get_defense(attacker, defender)
|
||||||
self.assertTrue(defense_roll == 50)
|
self.assertTrue(defense_roll == 50)
|
||||||
# Damage roll
|
# Damage roll
|
||||||
damage_roll = turnbattle.get_damage(attacker, defender)
|
damage_roll = tb_basic.get_damage(attacker, defender)
|
||||||
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
|
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
|
||||||
# Apply damage
|
# Apply damage
|
||||||
defender.db.hp = 10
|
defender.db.hp = 10
|
||||||
turnbattle.apply_damage(defender, 3)
|
tb_basic.apply_damage(defender, 3)
|
||||||
self.assertTrue(defender.db.hp == 7)
|
self.assertTrue(defender.db.hp == 7)
|
||||||
# Resolve attack
|
# Resolve attack
|
||||||
defender.db.hp = 40
|
defender.db.hp = 40
|
||||||
turnbattle.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
tb_basic.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
||||||
self.assertTrue(defender.db.hp < 40)
|
self.assertTrue(defender.db.hp < 40)
|
||||||
# Combat cleanup
|
# Combat cleanup
|
||||||
attacker.db.Combat_attribute = True
|
attacker.db.Combat_attribute = True
|
||||||
turnbattle.combat_cleanup(attacker)
|
tb_basic.combat_cleanup(attacker)
|
||||||
self.assertFalse(attacker.db.combat_attribute)
|
self.assertFalse(attacker.db.combat_attribute)
|
||||||
# Is in combat
|
# Is in combat
|
||||||
self.assertFalse(turnbattle.is_in_combat(attacker))
|
self.assertFalse(tb_basic.is_in_combat(attacker))
|
||||||
# Set up turn handler script for further tests
|
# Set up turn handler script for further tests
|
||||||
attacker.location.scripts.add(turnbattle.TurnHandler)
|
attacker.location.scripts.add(tb_basic.TBBasicTurnHandler)
|
||||||
turnhandler = attacker.db.combat_TurnHandler
|
turnhandler = attacker.db.combat_TurnHandler
|
||||||
self.assertTrue(attacker.db.combat_TurnHandler)
|
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||||
|
# Set the turn handler's interval very high to keep it from repeating during tests.
|
||||||
|
turnhandler.interval = 10000
|
||||||
# Force turn order
|
# Force turn order
|
||||||
turnhandler.db.fighters = [attacker, defender]
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
turnhandler.db.turn = 0
|
turnhandler.db.turn = 0
|
||||||
# Test is turn
|
# Test is turn
|
||||||
self.assertTrue(turnbattle.is_turn(attacker))
|
self.assertTrue(tb_basic.is_turn(attacker))
|
||||||
# Spend actions
|
# Spend actions
|
||||||
attacker.db.Combat_ActionsLeft = 1
|
attacker.db.Combat_ActionsLeft = 1
|
||||||
turnbattle.spend_action(attacker, 1, action_name="Test")
|
tb_basic.spend_action(attacker, 1, action_name="Test")
|
||||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||||
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||||
# Initialize for combat
|
# Initialize for combat
|
||||||
|
|
@ -1011,7 +1054,7 @@ class TestTurnBattleFunc(EvenniaTest):
|
||||||
turnhandler.turn_end_check(attacker)
|
turnhandler.turn_end_check(attacker)
|
||||||
self.assertTrue(turnhandler.db.turn == 1)
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
# Join fight
|
# Join fight
|
||||||
joiner = create_object(turnbattle.BattleCharacter, key="Joiner")
|
joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner")
|
||||||
turnhandler.db.fighters = [attacker, defender]
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
turnhandler.db.turn = 0
|
turnhandler.db.turn = 0
|
||||||
turnhandler.join_fight(joiner)
|
turnhandler.join_fight(joiner)
|
||||||
|
|
@ -1020,6 +1063,207 @@ class TestTurnBattleFunc(EvenniaTest):
|
||||||
# Remove the script at the end
|
# Remove the script at the end
|
||||||
turnhandler.stop()
|
turnhandler.stop()
|
||||||
|
|
||||||
|
# Test the combat functions in tb_equip too. They work mostly the same.
|
||||||
|
def test_tbequipfunc(self):
|
||||||
|
attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker")
|
||||||
|
defender = create_object(tb_equip.TBEquipCharacter, key="Defender")
|
||||||
|
testroom = create_object(DefaultRoom, key="Test Room")
|
||||||
|
attacker.location = testroom
|
||||||
|
defender.loaction = testroom
|
||||||
|
# Initiative roll
|
||||||
|
initiative = tb_equip.roll_init(attacker)
|
||||||
|
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||||
|
# Attack roll
|
||||||
|
attack_roll = tb_equip.get_attack(attacker, defender)
|
||||||
|
self.assertTrue(attack_roll >= -50 and attack_roll <= 150)
|
||||||
|
# Defense roll
|
||||||
|
defense_roll = tb_equip.get_defense(attacker, defender)
|
||||||
|
self.assertTrue(defense_roll == 50)
|
||||||
|
# Damage roll
|
||||||
|
damage_roll = tb_equip.get_damage(attacker, defender)
|
||||||
|
self.assertTrue(damage_roll >= 0 and damage_roll <= 50)
|
||||||
|
# Apply damage
|
||||||
|
defender.db.hp = 10
|
||||||
|
tb_equip.apply_damage(defender, 3)
|
||||||
|
self.assertTrue(defender.db.hp == 7)
|
||||||
|
# Resolve attack
|
||||||
|
defender.db.hp = 40
|
||||||
|
tb_equip.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
||||||
|
self.assertTrue(defender.db.hp < 40)
|
||||||
|
# Combat cleanup
|
||||||
|
attacker.db.Combat_attribute = True
|
||||||
|
tb_equip.combat_cleanup(attacker)
|
||||||
|
self.assertFalse(attacker.db.combat_attribute)
|
||||||
|
# Is in combat
|
||||||
|
self.assertFalse(tb_equip.is_in_combat(attacker))
|
||||||
|
# Set up turn handler script for further tests
|
||||||
|
attacker.location.scripts.add(tb_equip.TBEquipTurnHandler)
|
||||||
|
turnhandler = attacker.db.combat_TurnHandler
|
||||||
|
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||||
|
# Set the turn handler's interval very high to keep it from repeating during tests.
|
||||||
|
turnhandler.interval = 10000
|
||||||
|
# Force turn order
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
# Test is turn
|
||||||
|
self.assertTrue(tb_equip.is_turn(attacker))
|
||||||
|
# Spend actions
|
||||||
|
attacker.db.Combat_ActionsLeft = 1
|
||||||
|
tb_equip.spend_action(attacker, 1, action_name="Test")
|
||||||
|
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||||
|
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||||
|
# Initialize for combat
|
||||||
|
attacker.db.Combat_ActionsLeft = 983
|
||||||
|
turnhandler.initialize_for_combat(attacker)
|
||||||
|
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||||
|
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||||
|
# Start turn
|
||||||
|
defender.db.Combat_ActionsLeft = 0
|
||||||
|
turnhandler.start_turn(defender)
|
||||||
|
self.assertTrue(defender.db.Combat_ActionsLeft == 1)
|
||||||
|
# Next turn
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
turnhandler.next_turn()
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
# Turn end check
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
attacker.db.Combat_ActionsLeft = 0
|
||||||
|
turnhandler.turn_end_check(attacker)
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
# Join fight
|
||||||
|
joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner")
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
turnhandler.join_fight(joiner)
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||||
|
# Remove the script at the end
|
||||||
|
turnhandler.stop()
|
||||||
|
|
||||||
|
# Test combat functions in tb_range too.
|
||||||
|
def test_tbrangefunc(self):
|
||||||
|
testroom = create_object(DefaultRoom, key="Test Room")
|
||||||
|
attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=testroom)
|
||||||
|
defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=testroom)
|
||||||
|
# Initiative roll
|
||||||
|
initiative = tb_range.roll_init(attacker)
|
||||||
|
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||||
|
# Attack roll
|
||||||
|
attack_roll = tb_range.get_attack(attacker, defender, "test")
|
||||||
|
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
|
||||||
|
# Defense roll
|
||||||
|
defense_roll = tb_range.get_defense(attacker, defender, "test")
|
||||||
|
self.assertTrue(defense_roll == 50)
|
||||||
|
# Damage roll
|
||||||
|
damage_roll = tb_range.get_damage(attacker, defender)
|
||||||
|
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
|
||||||
|
# Apply damage
|
||||||
|
defender.db.hp = 10
|
||||||
|
tb_range.apply_damage(defender, 3)
|
||||||
|
self.assertTrue(defender.db.hp == 7)
|
||||||
|
# Resolve attack
|
||||||
|
defender.db.hp = 40
|
||||||
|
tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10)
|
||||||
|
self.assertTrue(defender.db.hp < 40)
|
||||||
|
# Combat cleanup
|
||||||
|
attacker.db.Combat_attribute = True
|
||||||
|
tb_range.combat_cleanup(attacker)
|
||||||
|
self.assertFalse(attacker.db.combat_attribute)
|
||||||
|
# Is in combat
|
||||||
|
self.assertFalse(tb_range.is_in_combat(attacker))
|
||||||
|
# Set up turn handler script for further tests
|
||||||
|
attacker.location.scripts.add(tb_range.TBRangeTurnHandler)
|
||||||
|
turnhandler = attacker.db.combat_TurnHandler
|
||||||
|
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||||
|
# Set the turn handler's interval very high to keep it from repeating during tests.
|
||||||
|
turnhandler.interval = 10000
|
||||||
|
# Force turn order
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
# Test is turn
|
||||||
|
self.assertTrue(tb_range.is_turn(attacker))
|
||||||
|
# Spend actions
|
||||||
|
attacker.db.Combat_ActionsLeft = 1
|
||||||
|
tb_range.spend_action(attacker, 1, action_name="Test")
|
||||||
|
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||||
|
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||||
|
# Initialize for combat
|
||||||
|
attacker.db.Combat_ActionsLeft = 983
|
||||||
|
turnhandler.initialize_for_combat(attacker)
|
||||||
|
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||||
|
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||||
|
# Set up ranges again, since initialize_for_combat clears them
|
||||||
|
attacker.db.combat_range = {}
|
||||||
|
attacker.db.combat_range[attacker] = 0
|
||||||
|
attacker.db.combat_range[defender] = 1
|
||||||
|
defender.db.combat_range = {}
|
||||||
|
defender.db.combat_range[defender] = 0
|
||||||
|
defender.db.combat_range[attacker] = 1
|
||||||
|
# Start turn
|
||||||
|
defender.db.Combat_ActionsLeft = 0
|
||||||
|
turnhandler.start_turn(defender)
|
||||||
|
self.assertTrue(defender.db.Combat_ActionsLeft == 2)
|
||||||
|
# Next turn
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
turnhandler.next_turn()
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
# Turn end check
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
attacker.db.Combat_ActionsLeft = 0
|
||||||
|
turnhandler.turn_end_check(attacker)
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
# Join fight
|
||||||
|
joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom)
|
||||||
|
turnhandler.db.fighters = [attacker, defender]
|
||||||
|
turnhandler.db.turn = 0
|
||||||
|
turnhandler.join_fight(joiner)
|
||||||
|
self.assertTrue(turnhandler.db.turn == 1)
|
||||||
|
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||||
|
# Now, test for approach/withdraw functions
|
||||||
|
self.assertTrue(tb_range.get_range(attacker, defender) == 1)
|
||||||
|
# Approach
|
||||||
|
tb_range.approach(attacker, defender)
|
||||||
|
self.assertTrue(tb_range.get_range(attacker, defender) == 0)
|
||||||
|
# Withdraw
|
||||||
|
tb_range.withdraw(attacker, defender)
|
||||||
|
self.assertTrue(tb_range.get_range(attacker, defender) == 1)
|
||||||
|
# Remove the script at the end
|
||||||
|
turnhandler.stop()
|
||||||
|
|
||||||
|
# Test tree select
|
||||||
|
|
||||||
|
from evennia.contrib import tree_select
|
||||||
|
|
||||||
|
TREE_MENU_TESTSTR = """Foo
|
||||||
|
Bar
|
||||||
|
-Baz
|
||||||
|
--Baz 1
|
||||||
|
--Baz 2
|
||||||
|
-Qux"""
|
||||||
|
|
||||||
|
class TestTreeSelectFunc(EvenniaTest):
|
||||||
|
|
||||||
|
def test_tree_functions(self):
|
||||||
|
# Dash counter
|
||||||
|
self.assertTrue(tree_select.dashcount("--test") == 2)
|
||||||
|
# Is category
|
||||||
|
self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) == True)
|
||||||
|
# Parse options
|
||||||
|
self.assertTrue(tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) == [(3, "Baz 1"), (4, "Baz 2")])
|
||||||
|
# Index to selection
|
||||||
|
self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz")
|
||||||
|
# Go up one category
|
||||||
|
self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2)
|
||||||
|
# Option list to menu options
|
||||||
|
test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2)
|
||||||
|
optlist_to_menu_expected_result = [{'goto': ['menunode_treeselect', {'newindex': 3}], 'key': 'Baz 1'},
|
||||||
|
{'goto': ['menunode_treeselect', {'newindex': 4}], 'key': 'Baz 2'},
|
||||||
|
{'goto': ['menunode_treeselect', {'newindex': 1}], 'key': ['<< Go Back', 'go back', 'back'], 'desc': 'Return to the previous menu.'}]
|
||||||
|
self.assertTrue(tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) == optlist_to_menu_expected_result)
|
||||||
|
|
||||||
# Test of the unixcommand module
|
# Test of the unixcommand module
|
||||||
|
|
||||||
|
|
|
||||||
535
evennia/contrib/tree_select.py
Normal file
535
evennia/contrib/tree_select.py
Normal file
|
|
@ -0,0 +1,535 @@
|
||||||
|
"""
|
||||||
|
Easy menu selection tree
|
||||||
|
|
||||||
|
Contrib - Tim Ashley Jenkins 2017
|
||||||
|
|
||||||
|
This module allows you to create and initialize an entire branching EvMenu
|
||||||
|
instance with nothing but a multi-line string passed to one function.
|
||||||
|
|
||||||
|
EvMenu is incredibly powerful and flexible, but using it for simple menus
|
||||||
|
can often be fairly cumbersome - a simple menu that can branch into five
|
||||||
|
categories would require six nodes, each with options represented as a list
|
||||||
|
of dictionaries.
|
||||||
|
|
||||||
|
This module provides a function, init_tree_selection, which acts as a frontend
|
||||||
|
for EvMenu, dynamically sourcing the options from a multi-line string you provide.
|
||||||
|
For example, if you define a string as such:
|
||||||
|
|
||||||
|
TEST_MENU = '''Foo
|
||||||
|
Bar
|
||||||
|
Baz
|
||||||
|
Qux'''
|
||||||
|
|
||||||
|
And then use TEST_MENU as the 'treestr' source when you call init_tree_selection
|
||||||
|
on a player:
|
||||||
|
|
||||||
|
init_tree_selection(TEST_MENU, caller, callback)
|
||||||
|
|
||||||
|
The player will be presented with an EvMenu, like so:
|
||||||
|
|
||||||
|
___________________________
|
||||||
|
|
||||||
|
Make your selection:
|
||||||
|
___________________________
|
||||||
|
|
||||||
|
Foo
|
||||||
|
Bar
|
||||||
|
Baz
|
||||||
|
Qux
|
||||||
|
|
||||||
|
Making a selection will pass the selection's key to the specified callback as a
|
||||||
|
string along with the caller, as well as the index of the selection (the line number
|
||||||
|
on the source string) along with the source string for the tree itself.
|
||||||
|
|
||||||
|
In addition to specifying selections on the menu, you can also specify categories.
|
||||||
|
Categories are indicated by putting options below it preceded with a '-' character.
|
||||||
|
If a selection is a category, then choosing it will bring up a new menu node, prompting
|
||||||
|
the player to select between those options, or to go back to the previous menu. In
|
||||||
|
addition, categories are marked by default with a '[+]' at the end of their key. Both
|
||||||
|
this marker and the option to go back can be disabled.
|
||||||
|
|
||||||
|
Categories can be nested in other categories as well - just go another '-' deeper. You
|
||||||
|
can do this as many times as you like. There's no hard limit to the number of
|
||||||
|
categories you can go down.
|
||||||
|
|
||||||
|
For example, let's add some more options to our menu, turning 'Bar' into a category.
|
||||||
|
|
||||||
|
TEST_MENU = '''Foo
|
||||||
|
Bar
|
||||||
|
-You've got to know
|
||||||
|
--When to hold em
|
||||||
|
--When to fold em
|
||||||
|
--When to walk away
|
||||||
|
Baz
|
||||||
|
Qux'''
|
||||||
|
|
||||||
|
Now when we call the menu, we can see that 'Bar' has become a category instead of a
|
||||||
|
selectable option.
|
||||||
|
|
||||||
|
_______________________________
|
||||||
|
|
||||||
|
Make your selection:
|
||||||
|
_______________________________
|
||||||
|
|
||||||
|
Foo
|
||||||
|
Bar [+]
|
||||||
|
Baz
|
||||||
|
Qux
|
||||||
|
|
||||||
|
Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it.
|
||||||
|
|
||||||
|
________________________________________________________________
|
||||||
|
|
||||||
|
Bar
|
||||||
|
________________________________________________________________
|
||||||
|
|
||||||
|
You've got to know [+]
|
||||||
|
<< Go Back: Return to the previous menu.
|
||||||
|
|
||||||
|
Just the one option, which is a category itself, and the option to go back, which will
|
||||||
|
take us back to the previous menu. Let's select 'You've got to know'.
|
||||||
|
|
||||||
|
________________________________________________________________
|
||||||
|
|
||||||
|
You've got to know
|
||||||
|
________________________________________________________________
|
||||||
|
|
||||||
|
When to hold em
|
||||||
|
When to fold em
|
||||||
|
When to walk away
|
||||||
|
<< Go Back: Return to the previous menu.
|
||||||
|
|
||||||
|
Now we see the three options listed under it, too. We can select one of them or use 'Go
|
||||||
|
Back' to return to the 'Bar' menu we were just at before. It's very simple to make a
|
||||||
|
branching tree of selections!
|
||||||
|
|
||||||
|
One last thing - you can set the descriptions for the various options simply by adding a
|
||||||
|
':' character followed by the description to the option's line. For example, let's add a
|
||||||
|
description to 'Baz' in our menu:
|
||||||
|
|
||||||
|
TEST_MENU = '''Foo
|
||||||
|
Bar
|
||||||
|
-You've got to know
|
||||||
|
--When to hold em
|
||||||
|
--When to fold em
|
||||||
|
--When to walk away
|
||||||
|
Baz: Look at this one: the best option.
|
||||||
|
Qux'''
|
||||||
|
|
||||||
|
Now we see that the Baz option has a description attached that's separate from its key:
|
||||||
|
|
||||||
|
_______________________________________________________________
|
||||||
|
|
||||||
|
Make your selection:
|
||||||
|
_______________________________________________________________
|
||||||
|
|
||||||
|
Foo
|
||||||
|
Bar [+]
|
||||||
|
Baz: Look at this one: the best option.
|
||||||
|
Qux
|
||||||
|
|
||||||
|
Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call
|
||||||
|
your specified callback with the selection, like so:
|
||||||
|
|
||||||
|
callback(caller, TEST_MENU, 0, "Foo")
|
||||||
|
|
||||||
|
The index of the selection is given along with a string containing the selection's key.
|
||||||
|
That way, if you have two selections in the menu with the same key, you can still
|
||||||
|
differentiate between them.
|
||||||
|
|
||||||
|
And that's all there is to it! For simple branching-tree selections, using this system is
|
||||||
|
much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic
|
||||||
|
options much easier - since the source of the menu tree is just a string, you could easily
|
||||||
|
generate that string procedurally before passing it to the init_tree_selection function.
|
||||||
|
For example, if a player casts a spell or does an attack without specifying a target, instead
|
||||||
|
of giving them an error, you could present them with a list of valid targets to select by
|
||||||
|
generating a multi-line string of targets and passing it to init_tree_selection, with the
|
||||||
|
callable performing the maneuver once a selection is made.
|
||||||
|
|
||||||
|
This selection system only works for simple branching trees - doing anything really complicated
|
||||||
|
like jumping between categories or prompting for arbitrary input would still require a full
|
||||||
|
EvMenu implementation. For simple selections, however, I'm sure you will find using this function
|
||||||
|
to be much easier!
|
||||||
|
|
||||||
|
Included in this module is a sample menu and function which will let a player change the color
|
||||||
|
of their name - feel free to mess with it to get a feel for how this system works by importing
|
||||||
|
this module in your game's default_cmdsets.py module and adding CmdNameColor to your default
|
||||||
|
character's command set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia.utils import evmenu
|
||||||
|
from evennia.utils.logger import log_trace
|
||||||
|
from evennia import Command
|
||||||
|
|
||||||
|
def init_tree_selection(treestr, caller, callback,
|
||||||
|
index=None, mark_category=True, go_back=True,
|
||||||
|
cmd_on_exit="look",
|
||||||
|
start_text="Make your selection:"):
|
||||||
|
"""
|
||||||
|
Prompts a player to select an option from a menu tree given as a multi-line string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-lne string representing menu options
|
||||||
|
caller (obj): Player to initialize the menu for
|
||||||
|
callback (callable): Function to run when a selection is made. Must take 4 args:
|
||||||
|
caller (obj): Caller given above
|
||||||
|
treestr (str): Menu tree string given above
|
||||||
|
index (int): Index of final selection
|
||||||
|
selection (str): Key of final selection
|
||||||
|
|
||||||
|
Options:
|
||||||
|
index (int or None): Index to start the menu at, or None for top level
|
||||||
|
mark_category (bool): If True, marks categories with a [+] symbol in the menu
|
||||||
|
go_back (bool): If True, present an option to go back to previous categories
|
||||||
|
start_text (str): Text to display at the top level of the menu
|
||||||
|
cmd_on_exit(str): Command to enter when the menu exits - 'look' by default
|
||||||
|
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This function will initialize an instance of EvMenu with options generated
|
||||||
|
dynamically from the source string, and passes the menu user's selection to
|
||||||
|
a function of your choosing. The EvMenu is made of a single, repeating node,
|
||||||
|
which will call itself over and over at different levels of the menu tree as
|
||||||
|
categories are selected.
|
||||||
|
|
||||||
|
Once a non-category selection is made, the user's selection will be passed to
|
||||||
|
the given callable, both as a string and as an index number. The index is given
|
||||||
|
to ensure every selection has a unique identifier, so that selections with the
|
||||||
|
same key in different categories can be distinguished between.
|
||||||
|
|
||||||
|
The menus called by this function are not persistent and cannot perform
|
||||||
|
complicated tasks like prompt for arbitrary input or jump multiple category
|
||||||
|
levels at once - you'll have to use EvMenu itself if you want to take full
|
||||||
|
advantage of its features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pass kwargs to store data needed in the menu
|
||||||
|
kwargs = {
|
||||||
|
"index":index,
|
||||||
|
"mark_category":mark_category,
|
||||||
|
"go_back":go_back,
|
||||||
|
"treestr":treestr,
|
||||||
|
"callback":callback,
|
||||||
|
"start_text":start_text
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize menu of selections
|
||||||
|
evmenu.EvMenu(caller, "evennia.contrib.tree_select", startnode="menunode_treeselect",
|
||||||
|
startnode_input=None, cmd_on_exit=cmd_on_exit, **kwargs)
|
||||||
|
|
||||||
|
def dashcount(entry):
|
||||||
|
"""
|
||||||
|
Counts the number of dashes at the beginning of a string. This
|
||||||
|
is needed to determine the depth of options in categories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry (str): String to count the dashes at the start of
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dashes (int): Number of dashes at the start
|
||||||
|
"""
|
||||||
|
dashes = 0
|
||||||
|
for char in entry:
|
||||||
|
if char == "-":
|
||||||
|
dashes += 1
|
||||||
|
else:
|
||||||
|
return dashes
|
||||||
|
return dashes
|
||||||
|
|
||||||
|
def is_category(treestr, index):
|
||||||
|
"""
|
||||||
|
Determines whether an option in a tree string is a category by
|
||||||
|
whether or not there are additional options below it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-line string representing menu options
|
||||||
|
index (int): Which line of the string to test
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
is_category (bool): Whether the option is a category
|
||||||
|
"""
|
||||||
|
opt_list = treestr.split('\n')
|
||||||
|
# Not a category if it's the last one in the list
|
||||||
|
if index == len(opt_list) - 1:
|
||||||
|
return False
|
||||||
|
# Not a category if next option is not one level deeper
|
||||||
|
return not bool(dashcount(opt_list[index+1]) != dashcount(opt_list[index]) + 1)
|
||||||
|
|
||||||
|
def parse_opts(treestr, category_index=None):
|
||||||
|
"""
|
||||||
|
Parses a tree string and given index into a list of options. If
|
||||||
|
category_index is none, returns all the options at the top level of
|
||||||
|
the menu. If category_index corresponds to a category, returns a list
|
||||||
|
of options under that category. If category_index corresponds to
|
||||||
|
an option that is not a category, it's a selection and returns True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-line string representing menu options
|
||||||
|
category_index (int): Index of category or None for top level
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
kept_opts (list or True): Either a list of options in the selected
|
||||||
|
category or True if a selection was made
|
||||||
|
"""
|
||||||
|
dash_depth = 0
|
||||||
|
opt_list = treestr.split('\n')
|
||||||
|
kept_opts = []
|
||||||
|
|
||||||
|
# If a category index is given
|
||||||
|
if category_index != None:
|
||||||
|
# If given index is not a category, it's a selection - return True.
|
||||||
|
if not is_category(treestr, category_index):
|
||||||
|
return True
|
||||||
|
# Otherwise, change the dash depth to match the new category.
|
||||||
|
dash_depth = dashcount(opt_list[category_index]) + 1
|
||||||
|
# Delete everything before the category index
|
||||||
|
opt_list = opt_list [category_index+1:]
|
||||||
|
|
||||||
|
# Keep every option (referenced by index) at the appropriate depth
|
||||||
|
cur_index = 0
|
||||||
|
for option in opt_list:
|
||||||
|
if dashcount(option) == dash_depth:
|
||||||
|
if category_index == None:
|
||||||
|
kept_opts.append((cur_index, option[dash_depth:]))
|
||||||
|
else:
|
||||||
|
kept_opts.append((cur_index + category_index + 1, option[dash_depth:]))
|
||||||
|
# Exits the loop if leaving a category
|
||||||
|
if dashcount(option) < dash_depth:
|
||||||
|
return kept_opts
|
||||||
|
cur_index += 1
|
||||||
|
return kept_opts
|
||||||
|
|
||||||
|
def index_to_selection(treestr, index, desc=False):
|
||||||
|
"""
|
||||||
|
Given a menu tree string and an index, returns the corresponding selection's
|
||||||
|
name as a string. If 'desc' is set to True, will return the selection's
|
||||||
|
description as a string instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-line string representing menu options
|
||||||
|
index (int): Index to convert to selection key or description
|
||||||
|
|
||||||
|
Options:
|
||||||
|
desc (bool): If true, returns description instead of key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
selection (str): Selection key or description if 'desc' is set
|
||||||
|
"""
|
||||||
|
opt_list = treestr.split('\n')
|
||||||
|
# Fetch the given line
|
||||||
|
selection = opt_list[index]
|
||||||
|
# Strip out the dashes at the start
|
||||||
|
selection = selection[dashcount(selection):]
|
||||||
|
# Separate out description, if any
|
||||||
|
if ":" in selection:
|
||||||
|
# Split string into key and description
|
||||||
|
selection = selection.split(':', 1)
|
||||||
|
selection[1] = selection[1].strip(" ")
|
||||||
|
else:
|
||||||
|
# If no description given, set description to None
|
||||||
|
selection = [selection, None]
|
||||||
|
if not desc:
|
||||||
|
return selection[0]
|
||||||
|
else:
|
||||||
|
return selection[1]
|
||||||
|
|
||||||
|
def go_up_one_category(treestr, index):
|
||||||
|
"""
|
||||||
|
Given a menu tree string and an index, returns the category that the given option
|
||||||
|
belongs to. Used for the 'go back' option.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-line string representing menu options
|
||||||
|
index (int): Index to determine the parent category of
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
parent_category (int): Index of parent category
|
||||||
|
"""
|
||||||
|
opt_list = treestr.split('\n')
|
||||||
|
# Get the number of dashes deep the given index is
|
||||||
|
dash_level = dashcount(opt_list[index])
|
||||||
|
# Delete everything after the current index
|
||||||
|
opt_list = opt_list[:index+1]
|
||||||
|
|
||||||
|
|
||||||
|
# If there's no dash, return 'None' to return to base menu
|
||||||
|
if dash_level == 0:
|
||||||
|
return None
|
||||||
|
current_index = index
|
||||||
|
# Go up through each option until we find one that's one category above
|
||||||
|
for selection in reversed(opt_list):
|
||||||
|
if dashcount(selection) == dash_level - 1:
|
||||||
|
return current_index
|
||||||
|
current_index -= 1
|
||||||
|
|
||||||
|
def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back):
|
||||||
|
"""
|
||||||
|
Takes a list of options processed by parse_opts and turns it into
|
||||||
|
a list/dictionary of menu options for use in menunode_treeselect.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
treestr (str): Multi-line string representing menu options
|
||||||
|
optlist (list): List of options to convert to EvMenu's option format
|
||||||
|
index (int): Index of current category
|
||||||
|
mark_category (bool): Whether or not to mark categories with [+]
|
||||||
|
go_back (bool): Whether or not to add an option to go back in the menu
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
menuoptions (list of dicts): List of menu options formatted for use
|
||||||
|
in EvMenu, each passing a different "newindex" kwarg that changes
|
||||||
|
the menu level or makes a selection
|
||||||
|
"""
|
||||||
|
|
||||||
|
menuoptions = []
|
||||||
|
cur_index = 0
|
||||||
|
for option in optlist:
|
||||||
|
index_to_add = optlist[cur_index][0]
|
||||||
|
menuitem = {}
|
||||||
|
keystr = index_to_selection(treestr, index_to_add)
|
||||||
|
if mark_category and is_category(treestr, index_to_add):
|
||||||
|
# Add the [+] to the key if marking categories, and the key by itself as an alias
|
||||||
|
menuitem["key"] = [keystr + " [+]", keystr]
|
||||||
|
else:
|
||||||
|
menuitem["key"] = keystr
|
||||||
|
# Get the option's description
|
||||||
|
desc = index_to_selection(treestr, index_to_add, desc=True)
|
||||||
|
if desc:
|
||||||
|
menuitem["desc"] = desc
|
||||||
|
# Passing 'newindex' as a kwarg to the node is how we move through the menu!
|
||||||
|
menuitem["goto"] = ["menunode_treeselect", {"newindex":index_to_add}]
|
||||||
|
menuoptions.append(menuitem)
|
||||||
|
cur_index += 1
|
||||||
|
# Add option to go back, if needed
|
||||||
|
if index != None and go_back == True:
|
||||||
|
gobackitem = {"key":["<< Go Back", "go back", "back"],
|
||||||
|
"desc":"Return to the previous menu.",
|
||||||
|
"goto":["menunode_treeselect", {"newindex":go_up_one_category(treestr, index)}]}
|
||||||
|
menuoptions.append(gobackitem)
|
||||||
|
return menuoptions
|
||||||
|
|
||||||
|
def menunode_treeselect(caller, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
This is the repeating menu node that handles the tree selection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If 'newindex' is in the kwargs, change the stored index.
|
||||||
|
if "newindex" in kwargs:
|
||||||
|
caller.ndb._menutree.index = kwargs["newindex"]
|
||||||
|
|
||||||
|
# Retrieve menu info
|
||||||
|
index = caller.ndb._menutree.index
|
||||||
|
mark_category = caller.ndb._menutree.mark_category
|
||||||
|
go_back = caller.ndb._menutree.go_back
|
||||||
|
treestr = caller.ndb._menutree.treestr
|
||||||
|
callback = caller.ndb._menutree.callback
|
||||||
|
start_text = caller.ndb._menutree.start_text
|
||||||
|
|
||||||
|
# List of options if index is 'None' or category, or 'True' if a selection
|
||||||
|
optlist = parse_opts(treestr, category_index=index)
|
||||||
|
|
||||||
|
# If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu.
|
||||||
|
if optlist == True:
|
||||||
|
selection = index_to_selection(treestr, index)
|
||||||
|
try:
|
||||||
|
callback(caller, treestr, index, selection)
|
||||||
|
except Exception:
|
||||||
|
log_trace("Error in tree selection callback.")
|
||||||
|
|
||||||
|
# Returning None, None ends the menu.
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Otherwise, convert optlist to a list of menu options.
|
||||||
|
else:
|
||||||
|
options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back)
|
||||||
|
if index == None:
|
||||||
|
# Use start_text for the menu text on the top level
|
||||||
|
text = start_text
|
||||||
|
else:
|
||||||
|
# Use the category name and description (if any) as the menu text
|
||||||
|
if index_to_selection(treestr, index, desc=True) != None:
|
||||||
|
text = "|w" + index_to_selection(treestr, index) + "|n: " + index_to_selection(treestr, index, desc=True)
|
||||||
|
else:
|
||||||
|
text = "|w" + index_to_selection(treestr, index) + "|n"
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
# The rest of this module is for the example menu and command! It'll change the color of your name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Here's an example string that you can initialize a menu from. Note the dashes at
|
||||||
|
the beginning of each line - that's how menu option depth and hierarchy is determined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NAMECOLOR_MENU = """Set name color: Choose a color for your name!
|
||||||
|
-Red shades: Various shades of |511red|n
|
||||||
|
--Red: |511Set your name to Red|n
|
||||||
|
--Pink: |533Set your name to Pink|n
|
||||||
|
--Maroon: |301Set your name to Maroon|n
|
||||||
|
-Orange shades: Various shades of |531orange|n
|
||||||
|
--Orange: |531Set your name to Orange|n
|
||||||
|
--Brown: |321Set your name to Brown|n
|
||||||
|
--Sienna: |420Set your name to Sienna|n
|
||||||
|
-Yellow shades: Various shades of |551yellow|n
|
||||||
|
--Yellow: |551Set your name to Yellow|n
|
||||||
|
--Gold: |540Set your name to Gold|n
|
||||||
|
--Dandelion: |553Set your name to Dandelion|n
|
||||||
|
-Green shades: Various shades of |141green|n
|
||||||
|
--Green: |141Set your name to Green|n
|
||||||
|
--Lime: |350Set your name to Lime|n
|
||||||
|
--Forest: |032Set your name to Forest|n
|
||||||
|
-Blue shades: Various shades of |115blue|n
|
||||||
|
--Blue: |115Set your name to Blue|n
|
||||||
|
--Cyan: |155Set your name to Cyan|n
|
||||||
|
--Navy: |113Set your name to Navy|n
|
||||||
|
-Purple shades: Various shades of |415purple|n
|
||||||
|
--Purple: |415Set your name to Purple|n
|
||||||
|
--Lavender: |535Set your name to Lavender|n
|
||||||
|
--Fuchsia: |503Set your name to Fuchsia|n
|
||||||
|
Remove name color: Remove your name color, if any"""
|
||||||
|
|
||||||
|
class CmdNameColor(Command):
|
||||||
|
"""
|
||||||
|
Set or remove a special color on your name. Just an example for the
|
||||||
|
easy menu selection tree contrib.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "namecolor"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
# This is all you have to do to initialize a menu!
|
||||||
|
init_tree_selection(NAMECOLOR_MENU, self.caller,
|
||||||
|
change_name_color,
|
||||||
|
start_text="Name color options:")
|
||||||
|
|
||||||
|
def change_name_color(caller, treestr, index, selection):
|
||||||
|
"""
|
||||||
|
Changes a player's name color.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (obj): Character whose name to color.
|
||||||
|
treestr (str): String for the color change menu - unused
|
||||||
|
index (int): Index of menu selection - unused
|
||||||
|
selection (str): Selection made from the name color menu - used
|
||||||
|
to determine the color the player chose.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Store the caller's uncolored name
|
||||||
|
if not caller.db.uncolored_name:
|
||||||
|
caller.db.uncolored_name = caller.key
|
||||||
|
|
||||||
|
# Dictionary matching color selection names to color codes
|
||||||
|
colordict = { "Red":"|511", "Pink":"|533", "Maroon":"|301",
|
||||||
|
"Orange":"|531", "Brown":"|321", "Sienna":"|420",
|
||||||
|
"Yellow":"|551", "Gold":"|540", "Dandelion":"|553",
|
||||||
|
"Green":"|141", "Lime":"|350", "Forest":"|032",
|
||||||
|
"Blue":"|115", "Cyan":"|155", "Navy":"|113",
|
||||||
|
"Purple":"|415", "Lavender":"|535", "Fuchsia":"|503"}
|
||||||
|
|
||||||
|
# I know this probably isn't the best way to do this. It's just an example!
|
||||||
|
if selection == "Remove name color": # Player chose to remove their name color
|
||||||
|
caller.key = caller.db.uncolored_name
|
||||||
|
caller.msg("Name color removed.")
|
||||||
|
elif selection in colordict:
|
||||||
|
newcolor = colordict[selection] # Retrieve color code based on menu selection
|
||||||
|
caller.key = newcolor + caller.db.uncolored_name + "|n" # Add color code to caller's name
|
||||||
|
caller.msg(newcolor + ("Name color changed to %s!" % selection) + "|n")
|
||||||
|
|
||||||
42
evennia/contrib/turnbattle/README.md
Normal file
42
evennia/contrib/turnbattle/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Turn based battle system framework
|
||||||
|
|
||||||
|
Contrib - Tim Ashley Jenkins 2017
|
||||||
|
|
||||||
|
This is a framework for a simple turn-based combat system, similar
|
||||||
|
to those used in D&D-style tabletop role playing games. It allows
|
||||||
|
any character to start a fight in a room, at which point initiative
|
||||||
|
is rolled and a turn order is established. Each participant in combat
|
||||||
|
has a limited time to decide their action for that turn (30 seconds by
|
||||||
|
default), and combat progresses through the turn order, looping through
|
||||||
|
the participants until the fight ends.
|
||||||
|
|
||||||
|
This folder contains multiple examples of how such a system can be
|
||||||
|
implemented and customized:
|
||||||
|
|
||||||
|
tb_basic.py - The simplest system, which implements initiative and turn
|
||||||
|
order, attack rolls against defense values, and damage to hit
|
||||||
|
points. Only very basic game mechanics are included.
|
||||||
|
|
||||||
|
tb_equip.py - Adds weapons and armor to the basic implementation of
|
||||||
|
the battle system, including commands for wielding weapons and
|
||||||
|
donning armor, and modifiers to accuracy and damage based on
|
||||||
|
currently used equipment.
|
||||||
|
|
||||||
|
tb_range.py - Adds a system for abstract positioning and movement, which
|
||||||
|
tracks the distance between different characters and objects in
|
||||||
|
combat, as well as differentiates between melee and ranged
|
||||||
|
attacks.
|
||||||
|
|
||||||
|
This system is meant as a basic framework to start from, and is modeled
|
||||||
|
after the combat systems of popular tabletop role playing games rather than
|
||||||
|
the real-time battle systems that many MMOs and some MUDs use. As such, it
|
||||||
|
may be better suited to role-playing or more story-oriented games, or games
|
||||||
|
meant to closely emulate the experience of playing a tabletop RPG.
|
||||||
|
|
||||||
|
Each of these modules contains the full functionality of the battle system
|
||||||
|
with different customizations added in - the instructions to install each
|
||||||
|
one is contained in the module itself. It's recommended that you install
|
||||||
|
and test tb_basic first, so you can better understand how the other
|
||||||
|
modules expand on it and get a better idea of how you can customize the
|
||||||
|
system to your liking and integrate the subsystems presented here into
|
||||||
|
your own combat system.
|
||||||
1
evennia/contrib/turnbattle/__init__.py
Normal file
1
evennia/contrib/turnbattle/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -16,26 +16,26 @@ is easily extensible and can be used as the foundation for implementing
|
||||||
the rules from your turn-based tabletop game of choice or making your
|
the rules from your turn-based tabletop game of choice or making your
|
||||||
own battle system.
|
own battle system.
|
||||||
|
|
||||||
To install and test, import this module's BattleCharacter object into
|
To install and test, import this module's TBBasicCharacter object into
|
||||||
your game's character.py module:
|
your game's character.py module:
|
||||||
|
|
||||||
from evennia.contrib.turnbattle import BattleCharacter
|
from evennia.contrib.turnbattle.tb_basic import TBBasicCharacter
|
||||||
|
|
||||||
And change your game's character typeclass to inherit from BattleCharacter
|
And change your game's character typeclass to inherit from TBBasicCharacter
|
||||||
instead of the default:
|
instead of the default:
|
||||||
|
|
||||||
class Character(BattleCharacter):
|
class Character(TBBasicCharacter):
|
||||||
|
|
||||||
Next, import this module into your default_cmdsets.py module:
|
Next, import this module into your default_cmdsets.py module:
|
||||||
|
|
||||||
from evennia.contrib import turnbattle
|
from evennia.contrib.turnbattle import tb_basic
|
||||||
|
|
||||||
And add the battle command set to your default command set:
|
And add the battle command set to your default command set:
|
||||||
|
|
||||||
#
|
#
|
||||||
# any commands you add below will overload the default ones.
|
# any commands you add below will overload the default ones.
|
||||||
#
|
#
|
||||||
self.add(turnbattle.BattleCmdSet())
|
self.add(tb_basic.BattleCmdSet())
|
||||||
|
|
||||||
This module is meant to be heavily expanded on, so you may want to copy it
|
This module is meant to be heavily expanded on, so you may want to copy it
|
||||||
to your game's 'world' folder and modify it there rather than importing it
|
to your game's 'world' folder and modify it there rather than importing it
|
||||||
|
|
@ -48,10 +48,18 @@ from evennia.commands.default.help import CmdHelp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
COMBAT FUNCTIONS START HERE
|
OPTIONS
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds
|
||||||
|
ACTIONS_PER_TURN = 1 # Number of actions allowed per turn
|
||||||
|
|
||||||
|
"""
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
COMBAT FUNCTIONS START HERE
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
|
||||||
def roll_init(character):
|
def roll_init(character):
|
||||||
"""
|
"""
|
||||||
|
|
@ -167,6 +175,20 @@ def apply_damage(defender, damage):
|
||||||
if defender.db.hp <= 0:
|
if defender.db.hp <= 0:
|
||||||
defender.db.hp = 0
|
defender.db.hp = 0
|
||||||
|
|
||||||
|
def at_defeat(defeated):
|
||||||
|
"""
|
||||||
|
Announces the defeat of a fighter in combat.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
defeated (obj): Fighter that's been defeated.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
All this does is announce a defeat message by default, but if you
|
||||||
|
want anything else to happen to defeated fighters (like putting them
|
||||||
|
into a dying state or something similar) then this is the place to
|
||||||
|
do it.
|
||||||
|
"""
|
||||||
|
defeated.location.msg_contents("%s has been defeated!" % defeated)
|
||||||
|
|
||||||
def resolve_attack(attacker, defender, attack_value=None, defense_value=None):
|
def resolve_attack(attacker, defender, attack_value=None, defense_value=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -195,10 +217,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None):
|
||||||
# Announce damage dealt and apply damage.
|
# Announce damage dealt and apply damage.
|
||||||
attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value))
|
attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value))
|
||||||
apply_damage(defender, damage_value)
|
apply_damage(defender, damage_value)
|
||||||
# If defender HP is reduced to 0 or less, announce defeat.
|
# If defender HP is reduced to 0 or less, call at_defeat.
|
||||||
if defender.db.hp <= 0:
|
if defender.db.hp <= 0:
|
||||||
attacker.location.msg_contents("%s has been defeated!" % defender)
|
at_defeat(defender)
|
||||||
|
|
||||||
|
|
||||||
def combat_cleanup(character):
|
def combat_cleanup(character):
|
||||||
"""
|
"""
|
||||||
|
|
@ -226,9 +247,7 @@ def is_in_combat(character):
|
||||||
Returns:
|
Returns:
|
||||||
(bool): True if in combat or False if not in combat
|
(bool): True if in combat or False if not in combat
|
||||||
"""
|
"""
|
||||||
if character.db.Combat_TurnHandler:
|
return bool(character.db.combat_turnhandler)
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_turn(character):
|
def is_turn(character):
|
||||||
|
|
@ -241,11 +260,9 @@ def is_turn(character):
|
||||||
Returns:
|
Returns:
|
||||||
(bool): True if it is their turn or False otherwise
|
(bool): True if it is their turn or False otherwise
|
||||||
"""
|
"""
|
||||||
turnhandler = character.db.Combat_TurnHandler
|
turnhandler = character.db.combat_turnhandler
|
||||||
currentchar = turnhandler.db.fighters[turnhandler.db.turn]
|
currentchar = turnhandler.db.fighters[turnhandler.db.turn]
|
||||||
if character == currentchar:
|
return bool(character == currentchar)
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def spend_action(character, actions, action_name=None):
|
def spend_action(character, actions, action_name=None):
|
||||||
|
|
@ -261,14 +278,14 @@ def spend_action(character, actions, action_name=None):
|
||||||
combat to provided string
|
combat to provided string
|
||||||
"""
|
"""
|
||||||
if action_name:
|
if action_name:
|
||||||
character.db.Combat_LastAction = action_name
|
character.db.combat_lastaction = action_name
|
||||||
if actions == 'all': # If spending all actions
|
if actions == 'all': # If spending all actions
|
||||||
character.db.Combat_ActionsLeft = 0 # Set actions to 0
|
character.db.combat_actionsleft = 0 # Set actions to 0
|
||||||
else:
|
else:
|
||||||
character.db.Combat_ActionsLeft -= actions # Use up actions.
|
character.db.combat_actionsleft -= actions # Use up actions.
|
||||||
if character.db.Combat_ActionsLeft < 0:
|
if character.db.combat_actionsleft < 0:
|
||||||
character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions
|
character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions
|
||||||
character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn.
|
character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -278,7 +295,7 @@ CHARACTER TYPECLASS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BattleCharacter(DefaultCharacter):
|
class TBBasicCharacter(DefaultCharacter):
|
||||||
"""
|
"""
|
||||||
A character able to participate in turn-based combat. Has attributes for current
|
A character able to participate in turn-based combat. Has attributes for current
|
||||||
and maximum HP, and access to combat commands.
|
and maximum HP, and access to combat commands.
|
||||||
|
|
@ -324,6 +341,181 @@ class BattleCharacter(DefaultCharacter):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
"""
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
SCRIPTS START HERE
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TBBasicTurnHandler(DefaultScript):
|
||||||
|
"""
|
||||||
|
This is the script that handles the progression of combat through turns.
|
||||||
|
On creation (when a fight is started) it adds all combat-ready characters
|
||||||
|
to its roster and then sorts them into a turn order. There can only be one
|
||||||
|
fight going on in a single room at a time, so the script is assigned to a
|
||||||
|
room as its object.
|
||||||
|
|
||||||
|
Fights persist until only one participant is left with any HP or all
|
||||||
|
remaining participants choose to end the combat with the 'disengage' command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def at_script_creation(self):
|
||||||
|
"""
|
||||||
|
Called once, when the script is created.
|
||||||
|
"""
|
||||||
|
self.key = "Combat Turn Handler"
|
||||||
|
self.interval = 5 # Once every 5 seconds
|
||||||
|
self.persistent = True
|
||||||
|
self.db.fighters = []
|
||||||
|
|
||||||
|
# Add all fighters in the room with at least 1 HP to the combat."
|
||||||
|
for thing in self.obj.contents:
|
||||||
|
if thing.db.hp:
|
||||||
|
self.db.fighters.append(thing)
|
||||||
|
|
||||||
|
# Initialize each fighter for combat
|
||||||
|
for fighter in self.db.fighters:
|
||||||
|
self.initialize_for_combat(fighter)
|
||||||
|
|
||||||
|
# Add a reference to this script to the room
|
||||||
|
self.obj.db.combat_turnhandler = self
|
||||||
|
|
||||||
|
# Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order.
|
||||||
|
# The initiative roll is determined by the roll_init function and can be customized easily.
|
||||||
|
ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True)
|
||||||
|
self.db.fighters = ordered_by_roll
|
||||||
|
|
||||||
|
# Announce the turn order.
|
||||||
|
self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters))
|
||||||
|
|
||||||
|
# Start first fighter's turn.
|
||||||
|
self.start_turn(self.db.fighters[0])
|
||||||
|
|
||||||
|
# Set up the current turn and turn timeout delay.
|
||||||
|
self.db.turn = 0
|
||||||
|
self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options
|
||||||
|
|
||||||
|
def at_stop(self):
|
||||||
|
"""
|
||||||
|
Called at script termination.
|
||||||
|
"""
|
||||||
|
for fighter in self.db.fighters:
|
||||||
|
combat_cleanup(fighter) # Clean up the combat attributes for every fighter.
|
||||||
|
self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location
|
||||||
|
|
||||||
|
def at_repeat(self):
|
||||||
|
"""
|
||||||
|
Called once every self.interval seconds.
|
||||||
|
"""
|
||||||
|
currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order.
|
||||||
|
self.db.timer -= self.interval # Count down the timer.
|
||||||
|
|
||||||
|
if self.db.timer <= 0:
|
||||||
|
# Force current character to disengage if timer runs out.
|
||||||
|
self.obj.msg_contents("%s's turn timed out!" % currentchar)
|
||||||
|
spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions.
|
||||||
|
return
|
||||||
|
elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left
|
||||||
|
# Warn the current character if they're about to time out.
|
||||||
|
currentchar.msg("WARNING: About to time out!")
|
||||||
|
self.db.timeout_warning_given = True
|
||||||
|
|
||||||
|
def initialize_for_combat(self, character):
|
||||||
|
"""
|
||||||
|
Prepares a character for combat when starting or entering a fight.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character (obj): Character to initialize for combat.
|
||||||
|
"""
|
||||||
|
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||||
|
character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||||
|
character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character
|
||||||
|
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||||
|
|
||||||
|
def start_turn(self, character):
|
||||||
|
"""
|
||||||
|
Readies a character for the start of their turn by replenishing their
|
||||||
|
available actions and notifying them that their turn has come up.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character (obj): Character to be readied.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Here, you only get one action per turn, but you might want to allow more than
|
||||||
|
one per turn, or even grant a number of actions based on a character's
|
||||||
|
attributes. You can even add multiple different kinds of actions, I.E. actions
|
||||||
|
separated for movement, by adding "character.db.combat_movesleft = 3" or
|
||||||
|
something similar.
|
||||||
|
"""
|
||||||
|
character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions
|
||||||
|
# Prompt the character for their turn and give some information.
|
||||||
|
character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp)
|
||||||
|
|
||||||
|
def next_turn(self):
|
||||||
|
"""
|
||||||
|
Advances to the next character in the turn order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check to see if every character disengaged as their last action. If so, end combat.
|
||||||
|
disengage_check = True
|
||||||
|
for fighter in self.db.fighters:
|
||||||
|
if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage
|
||||||
|
disengage_check = False
|
||||||
|
if disengage_check: # All characters have disengaged
|
||||||
|
self.obj.msg_contents("All fighters have disengaged! Combat is over!")
|
||||||
|
self.stop() # Stop this script and end combat.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check to see if only one character is left standing. If so, end combat.
|
||||||
|
defeated_characters = 0
|
||||||
|
for fighter in self.db.fighters:
|
||||||
|
if fighter.db.HP == 0:
|
||||||
|
defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated)
|
||||||
|
if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated
|
||||||
|
for fighter in self.db.fighters:
|
||||||
|
if fighter.db.HP != 0:
|
||||||
|
LastStanding = fighter # Pick the one fighter left with HP remaining
|
||||||
|
self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
|
||||||
|
self.stop() # Stop this script and end combat.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cycle to the next turn.
|
||||||
|
currentchar = self.db.fighters[self.db.turn]
|
||||||
|
self.db.turn += 1 # Go to the next in the turn order.
|
||||||
|
if self.db.turn > len(self.db.fighters) - 1:
|
||||||
|
self.db.turn = 0 # Go back to the first in the turn order once you reach the end.
|
||||||
|
newchar = self.db.fighters[self.db.turn] # Note the new character
|
||||||
|
self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer.
|
||||||
|
self.db.timeout_warning_given = False # Reset the timeout warning.
|
||||||
|
self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar))
|
||||||
|
self.start_turn(newchar) # Start the new character's turn.
|
||||||
|
|
||||||
|
def turn_end_check(self, character):
|
||||||
|
"""
|
||||||
|
Tests to see if a character's turn is over, and cycles to the next turn if it is.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character (obj): Character to test for end of turn
|
||||||
|
"""
|
||||||
|
if not character.db.combat_actionsleft: # Character has no actions remaining
|
||||||
|
self.next_turn()
|
||||||
|
return
|
||||||
|
|
||||||
|
def join_fight(self, character):
|
||||||
|
"""
|
||||||
|
Adds a new character to a fight already in progress.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character (obj): Character to be added to the fight.
|
||||||
|
"""
|
||||||
|
# Inserts the fighter to the turn order, right behind whoever's turn it currently is.
|
||||||
|
self.db.fighters.insert(self.db.turn, character)
|
||||||
|
# Tick the turn counter forward one to compensate.
|
||||||
|
self.db.turn += 1
|
||||||
|
# Initialize the character like you do at the start.
|
||||||
|
self.initialize_for_combat(character)
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
@ -365,13 +557,13 @@ class CmdFight(Command):
|
||||||
if len(fighters) <= 1: # If you're the only able fighter in the room
|
if len(fighters) <= 1: # If you're the only able fighter in the room
|
||||||
self.caller.msg("There's nobody here to fight!")
|
self.caller.msg("There's nobody here to fight!")
|
||||||
return
|
return
|
||||||
if here.db.Combat_TurnHandler: # If there's already a fight going on...
|
if here.db.combat_turnhandler: # If there's already a fight going on...
|
||||||
here.msg_contents("%s joins the fight!" % self.caller)
|
here.msg_contents("%s joins the fight!" % self.caller)
|
||||||
here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight!
|
here.db.combat_turnhandler.join_fight(self.caller) # Join the fight!
|
||||||
return
|
return
|
||||||
here.msg_contents("%s starts a fight!" % self.caller)
|
here.msg_contents("%s starts a fight!" % self.caller)
|
||||||
# Add a turn handler script to the room, which starts combat.
|
# Add a turn handler script to the room, which starts combat.
|
||||||
here.scripts.add("contrib.turnbattle.TurnHandler")
|
here.scripts.add("contrib.turnbattle.tb_basic.TBBasicTurnHandler")
|
||||||
# Remember you'll have to change the path to the script if you copy this code to your own modules!
|
# Remember you'll have to change the path to the script if you copy this code to your own modules!
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -560,176 +752,3 @@ class BattleCmdSet(default_cmds.CharacterCmdSet):
|
||||||
self.add(CmdPass())
|
self.add(CmdPass())
|
||||||
self.add(CmdDisengage())
|
self.add(CmdDisengage())
|
||||||
self.add(CmdCombatHelp())
|
self.add(CmdCombatHelp())
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
SCRIPTS START HERE
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TurnHandler(DefaultScript):
|
|
||||||
"""
|
|
||||||
This is the script that handles the progression of combat through turns.
|
|
||||||
On creation (when a fight is started) it adds all combat-ready characters
|
|
||||||
to its roster and then sorts them into a turn order. There can only be one
|
|
||||||
fight going on in a single room at a time, so the script is assigned to a
|
|
||||||
room as its object.
|
|
||||||
|
|
||||||
Fights persist until only one participant is left with any HP or all
|
|
||||||
remaining participants choose to end the combat with the 'disengage' command.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"""
|
|
||||||
Called once, when the script is created.
|
|
||||||
"""
|
|
||||||
self.key = "Combat Turn Handler"
|
|
||||||
self.interval = 5 # Once every 5 seconds
|
|
||||||
self.persistent = True
|
|
||||||
self.db.fighters = []
|
|
||||||
|
|
||||||
# Add all fighters in the room with at least 1 HP to the combat."
|
|
||||||
for object in self.obj.contents:
|
|
||||||
if object.db.hp:
|
|
||||||
self.db.fighters.append(object)
|
|
||||||
|
|
||||||
# Initialize each fighter for combat
|
|
||||||
for fighter in self.db.fighters:
|
|
||||||
self.initialize_for_combat(fighter)
|
|
||||||
|
|
||||||
# Add a reference to this script to the room
|
|
||||||
self.obj.db.Combat_TurnHandler = self
|
|
||||||
|
|
||||||
# Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order.
|
|
||||||
# The initiative roll is determined by the roll_init function and can be customized easily.
|
|
||||||
ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True)
|
|
||||||
self.db.fighters = ordered_by_roll
|
|
||||||
|
|
||||||
# Announce the turn order.
|
|
||||||
self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters))
|
|
||||||
|
|
||||||
# Set up the current turn and turn timeout delay.
|
|
||||||
self.db.turn = 0
|
|
||||||
self.db.timer = 30 # 30 seconds
|
|
||||||
|
|
||||||
def at_stop(self):
|
|
||||||
"""
|
|
||||||
Called at script termination.
|
|
||||||
"""
|
|
||||||
for fighter in self.db.fighters:
|
|
||||||
combat_cleanup(fighter) # Clean up the combat attributes for every fighter.
|
|
||||||
self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location
|
|
||||||
|
|
||||||
def at_repeat(self):
|
|
||||||
"""
|
|
||||||
Called once every self.interval seconds.
|
|
||||||
"""
|
|
||||||
currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order.
|
|
||||||
self.db.timer -= self.interval # Count down the timer.
|
|
||||||
|
|
||||||
if self.db.timer <= 0:
|
|
||||||
# Force current character to disengage if timer runs out.
|
|
||||||
self.obj.msg_contents("%s's turn timed out!" % currentchar)
|
|
||||||
spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions.
|
|
||||||
return
|
|
||||||
elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left
|
|
||||||
# Warn the current character if they're about to time out.
|
|
||||||
currentchar.msg("WARNING: About to time out!")
|
|
||||||
self.db.timeout_warning_given = True
|
|
||||||
|
|
||||||
def initialize_for_combat(self, character):
|
|
||||||
"""
|
|
||||||
Prepares a character for combat when starting or entering a fight.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
character (obj): Character to initialize for combat.
|
|
||||||
"""
|
|
||||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
|
||||||
character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
|
||||||
character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character
|
|
||||||
character.db.Combat_LastAction = "null" # Track last action taken in combat
|
|
||||||
|
|
||||||
def start_turn(self, character):
|
|
||||||
"""
|
|
||||||
Readies a character for the start of their turn by replenishing their
|
|
||||||
available actions and notifying them that their turn has come up.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
character (obj): Character to be readied.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Here, you only get one action per turn, but you might want to allow more than
|
|
||||||
one per turn, or even grant a number of actions based on a character's
|
|
||||||
attributes. You can even add multiple different kinds of actions, I.E. actions
|
|
||||||
separated for movement, by adding "character.db.Combat_MovesLeft = 3" or
|
|
||||||
something similar.
|
|
||||||
"""
|
|
||||||
character.db.Combat_ActionsLeft = 1 # 1 action per turn.
|
|
||||||
# Prompt the character for their turn and give some information.
|
|
||||||
character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp)
|
|
||||||
|
|
||||||
def next_turn(self):
|
|
||||||
"""
|
|
||||||
Advances to the next character in the turn order.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Check to see if every character disengaged as their last action. If so, end combat.
|
|
||||||
disengage_check = True
|
|
||||||
for fighter in self.db.fighters:
|
|
||||||
if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage
|
|
||||||
disengage_check = False
|
|
||||||
if disengage_check: # All characters have disengaged
|
|
||||||
self.obj.msg_contents("All fighters have disengaged! Combat is over!")
|
|
||||||
self.stop() # Stop this script and end combat.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check to see if only one character is left standing. If so, end combat.
|
|
||||||
defeated_characters = 0
|
|
||||||
for fighter in self.db.fighters:
|
|
||||||
if fighter.db.HP == 0:
|
|
||||||
defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated)
|
|
||||||
if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated
|
|
||||||
for fighter in self.db.fighters:
|
|
||||||
if fighter.db.HP != 0:
|
|
||||||
LastStanding = fighter # Pick the one fighter left with HP remaining
|
|
||||||
self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
|
|
||||||
self.stop() # Stop this script and end combat.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Cycle to the next turn.
|
|
||||||
currentchar = self.db.fighters[self.db.turn]
|
|
||||||
self.db.turn += 1 # Go to the next in the turn order.
|
|
||||||
if self.db.turn > len(self.db.fighters) - 1:
|
|
||||||
self.db.turn = 0 # Go back to the first in the turn order once you reach the end.
|
|
||||||
newchar = self.db.fighters[self.db.turn] # Note the new character
|
|
||||||
self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer.
|
|
||||||
self.db.timeout_warning_given = False # Reset the timeout warning.
|
|
||||||
self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar))
|
|
||||||
self.start_turn(newchar) # Start the new character's turn.
|
|
||||||
|
|
||||||
def turn_end_check(self, character):
|
|
||||||
"""
|
|
||||||
Tests to see if a character's turn is over, and cycles to the next turn if it is.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
character (obj): Character to test for end of turn
|
|
||||||
"""
|
|
||||||
if not character.db.Combat_ActionsLeft: # Character has no actions remaining
|
|
||||||
self.next_turn()
|
|
||||||
return
|
|
||||||
|
|
||||||
def join_fight(self, character):
|
|
||||||
"""
|
|
||||||
Adds a new character to a fight already in progress.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
character (obj): Character to be added to the fight.
|
|
||||||
"""
|
|
||||||
# Inserts the fighter to the turn order, right behind whoever's turn it currently is.
|
|
||||||
self.db.fighters.insert(self.db.turn, character)
|
|
||||||
# Tick the turn counter forward one to compensate.
|
|
||||||
self.db.turn += 1
|
|
||||||
# Initialize the character like you do at the start.
|
|
||||||
self.initialize_for_combat(character)
|
|
||||||
1086
evennia/contrib/turnbattle/tb_equip.py
Normal file
1086
evennia/contrib/turnbattle/tb_equip.py
Normal file
File diff suppressed because it is too large
Load diff
1383
evennia/contrib/turnbattle/tb_range.py
Normal file
1383
evennia/contrib/turnbattle/tb_range.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1497,6 +1497,25 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_before_get(self, getter, **kwargs):
|
||||||
|
"""
|
||||||
|
Called by the default `get` command before this object has been
|
||||||
|
picked up.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
getter (Object): The object about to get this object.
|
||||||
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
|
overriding the call (unused by default).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
shouldget (bool): If the object should be gotten or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If this method returns False/None, the getting is cancelled
|
||||||
|
before it is even started.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def at_get(self, getter, **kwargs):
|
def at_get(self, getter, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called by the default `get` command when this object has been
|
Called by the default `get` command when this object has been
|
||||||
|
|
@ -1509,11 +1528,32 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This hook cannot stop the pickup from happening. Use
|
This hook cannot stop the pickup from happening. Use
|
||||||
permissions for that.
|
permissions or the at_before_get() hook for that.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_before_give(self, giver, getter, **kwargs):
|
||||||
|
"""
|
||||||
|
Called by the default `give` command before this object has been
|
||||||
|
given.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
giver (Object): The object about to give this object.
|
||||||
|
getter (Object): The object about to get this object.
|
||||||
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
|
overriding the call (unused by default).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
shouldgive (bool): If the object should be given or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If this method returns False/None, the giving is cancelled
|
||||||
|
before it is even started.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def at_give(self, giver, getter, **kwargs):
|
def at_give(self, giver, getter, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called by the default `give` command when this object has been
|
Called by the default `give` command when this object has been
|
||||||
|
|
@ -1527,11 +1567,31 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This hook cannot stop the give from happening. Use
|
This hook cannot stop the give from happening. Use
|
||||||
permissions for that.
|
permissions or the at_before_give() hook for that.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_before_drop(self, dropper, **kwargs):
|
||||||
|
"""
|
||||||
|
Called by the default `drop` command before this object has been
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dropper (Object): The object which will drop this object.
|
||||||
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
|
overriding the call (unused by default).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
shoulddrop (bool): If the object should be dropped or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If this method returns False/None, the dropping is cancelled
|
||||||
|
before it is even started.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def at_drop(self, dropper, **kwargs):
|
def at_drop(self, dropper, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called by the default `drop` command when this object has been
|
Called by the default `drop` command when this object has been
|
||||||
|
|
@ -1544,7 +1604,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This hook cannot stop the drop from happening. Use
|
This hook cannot stop the drop from happening. Use
|
||||||
permissions from that.
|
permissions or the at_before_drop() hook for that.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
@ -1624,11 +1684,11 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
# whisper mode
|
# whisper mode
|
||||||
msg_type = 'whisper'
|
msg_type = 'whisper'
|
||||||
msg_self = '{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self
|
msg_self = '{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self
|
||||||
|
msg_receivers = '{object} whispers: "{speech}"'
|
||||||
msg_receivers = msg_receivers or '{object} whispers: "{speech}"'
|
msg_receivers = msg_receivers or '{object} whispers: "{speech}"'
|
||||||
msg_location = None
|
msg_location = None
|
||||||
else:
|
else:
|
||||||
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
|
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
|
||||||
msg_receivers = None
|
|
||||||
msg_location = msg_location or '{object} says, "{speech}"'
|
msg_location = msg_location or '{object} says, "{speech}"'
|
||||||
|
|
||||||
custom_mapping = kwargs.get('mapping', {})
|
custom_mapping = kwargs.get('mapping', {})
|
||||||
|
|
@ -1673,9 +1733,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
"receiver": None,
|
"receiver": None,
|
||||||
"speech": message}
|
"speech": message}
|
||||||
location_mapping.update(custom_mapping)
|
location_mapping.update(custom_mapping)
|
||||||
|
exclude = []
|
||||||
|
if msg_self:
|
||||||
|
exclude.append(self)
|
||||||
|
if receivers:
|
||||||
|
exclude.extend(receivers)
|
||||||
self.location.msg_contents(text=(msg_location, {"type": msg_type}),
|
self.location.msg_contents(text=(msg_location, {"type": msg_type}),
|
||||||
from_obj=self,
|
from_obj=self,
|
||||||
exclude=(self, ) if msg_self else None,
|
exclude=exclude,
|
||||||
mapping=location_mapping)
|
mapping=location_mapping)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
VALIDATE_ITERATION -= 1
|
VALIDATE_ITERATION -= 1
|
||||||
return nr_started, nr_stopped
|
return nr_started, nr_stopped
|
||||||
|
|
||||||
def search_script(self, ostring, obj=None, only_timed=False):
|
def search_script(self, ostring, obj=None, only_timed=False, typeclass=None):
|
||||||
"""
|
"""
|
||||||
Search for a particular script.
|
Search for a particular script.
|
||||||
|
|
||||||
|
|
@ -224,6 +224,7 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
this object
|
this object
|
||||||
only_timed (bool): Limit search only to scripts that run
|
only_timed (bool): Limit search only to scripts that run
|
||||||
on a timer.
|
on a timer.
|
||||||
|
typeclass (class or str): Typeclass or path to typeclass.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -237,10 +238,17 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
(only_timed and dbref_match.interval)):
|
(only_timed and dbref_match.interval)):
|
||||||
return [dbref_match]
|
return [dbref_match]
|
||||||
|
|
||||||
|
if typeclass:
|
||||||
|
if callable(typeclass):
|
||||||
|
typeclass = u"%s.%s" % (typeclass.__module__, typeclass.__name__)
|
||||||
|
else:
|
||||||
|
typeclass = u"%s" % typeclass
|
||||||
|
|
||||||
# not a dbref; normal search
|
# not a dbref; normal search
|
||||||
obj_restriction = obj and Q(db_obj=obj) or Q()
|
obj_restriction = obj and Q(db_obj=obj) or Q()
|
||||||
timed_restriction = only_timed and Q(interval__gt=0) or Q()
|
timed_restriction = only_timed and Q(db_interval__gt=0) or Q()
|
||||||
scripts = self.filter(timed_restriction & obj_restriction & Q(db_key__iexact=ostring))
|
typeclass_restriction = typeclass and Q(db_typeclass_path=typeclass) or Q()
|
||||||
|
scripts = self.filter(timed_restriction & obj_restriction & typeclass_restriction & Q(db_key__iexact=ostring))
|
||||||
return scripts
|
return scripts
|
||||||
# back-compatibility alias
|
# back-compatibility alias
|
||||||
script_search = search_script
|
script_search = search_script
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ if AMP_ENABLED:
|
||||||
|
|
||||||
from evennia.server import amp
|
from evennia.server import amp
|
||||||
|
|
||||||
print(' amp (to Server): %s' % AMP_PORT)
|
print(' amp (to Server): %s (internal)' % AMP_PORT)
|
||||||
|
|
||||||
factory = amp.AmpClientFactory(PORTAL)
|
factory = amp.AmpClientFactory(PORTAL)
|
||||||
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
||||||
|
|
@ -223,7 +223,7 @@ if TELNET_ENABLED:
|
||||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||||
PORTAL.services.addService(telnet_service)
|
PORTAL.services.addService(telnet_service)
|
||||||
|
|
||||||
print(' telnet%s: %s' % (ifacestr, port))
|
print(' telnet%s: %s (external)' % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if SSL_ENABLED:
|
if SSL_ENABLED:
|
||||||
|
|
@ -249,7 +249,7 @@ if SSL_ENABLED:
|
||||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||||
PORTAL.services.addService(ssl_service)
|
PORTAL.services.addService(ssl_service)
|
||||||
|
|
||||||
print(" ssl%s: %s" % (ifacestr, port))
|
print(" ssl%s: %s (external)" % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if SSH_ENABLED:
|
if SSH_ENABLED:
|
||||||
|
|
@ -273,7 +273,7 @@ if SSH_ENABLED:
|
||||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||||
PORTAL.services.addService(ssh_service)
|
PORTAL.services.addService(ssh_service)
|
||||||
|
|
||||||
print(" ssh%s: %s" % (ifacestr, port))
|
print(" ssh%s: %s (external)" % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if WEBSERVER_ENABLED:
|
if WEBSERVER_ENABLED:
|
||||||
|
|
@ -287,7 +287,6 @@ if WEBSERVER_ENABLED:
|
||||||
if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
||||||
ifacestr = "-%s" % interface
|
ifacestr = "-%s" % interface
|
||||||
for proxyport, serverport in WEBSERVER_PORTS:
|
for proxyport, serverport in WEBSERVER_PORTS:
|
||||||
pstring = "%s:%s<->%s" % (ifacestr, proxyport, serverport)
|
|
||||||
web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '')
|
web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '')
|
||||||
webclientstr = ""
|
webclientstr = ""
|
||||||
if WEBCLIENT_ENABLED:
|
if WEBCLIENT_ENABLED:
|
||||||
|
|
@ -305,21 +304,20 @@ if WEBSERVER_ENABLED:
|
||||||
from evennia.server.portal import webclient
|
from evennia.server.portal import webclient
|
||||||
from evennia.utils.txws import WebSocketFactory
|
from evennia.utils.txws import WebSocketFactory
|
||||||
|
|
||||||
interface = WEBSOCKET_CLIENT_INTERFACE
|
w_interface = WEBSOCKET_CLIENT_INTERFACE
|
||||||
|
w_ifacestr = ''
|
||||||
|
if w_interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
||||||
|
w_ifacestr = "-%s" % interface
|
||||||
port = WEBSOCKET_CLIENT_PORT
|
port = WEBSOCKET_CLIENT_PORT
|
||||||
ifacestr = ""
|
|
||||||
if interface not in ('0.0.0.0', '::'):
|
|
||||||
ifacestr = "-%s" % interface
|
|
||||||
pstring = "%s:%s" % (ifacestr, port)
|
|
||||||
factory = protocol.ServerFactory()
|
factory = protocol.ServerFactory()
|
||||||
factory.noisy = False
|
factory.noisy = False
|
||||||
factory.protocol = webclient.WebSocketClient
|
factory.protocol = webclient.WebSocketClient
|
||||||
factory.sessionhandler = PORTAL_SESSIONS
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
|
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=w_interface)
|
||||||
websocket_service.setName('EvenniaWebSocket%s' % pstring)
|
websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, proxyport))
|
||||||
PORTAL.services.addService(websocket_service)
|
PORTAL.services.addService(websocket_service)
|
||||||
websocket_started = True
|
websocket_started = True
|
||||||
webclientstr = "\n + webclient%s" % pstring
|
webclientstr = "\n + webclient-websocket%s: %s (external)" % (w_ifacestr, proxyport)
|
||||||
|
|
||||||
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||||
proxy_service = internet.TCPServer(proxyport,
|
proxy_service = internet.TCPServer(proxyport,
|
||||||
|
|
@ -327,7 +325,7 @@ if WEBSERVER_ENABLED:
|
||||||
interface=interface)
|
interface=interface)
|
||||||
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
||||||
PORTAL.services.addService(proxy_service)
|
PORTAL.services.addService(proxy_service)
|
||||||
print(" webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr))
|
print(" website-proxy%s: %s (external) %s" % (ifacestr, proxyport, webclientstr))
|
||||||
|
|
||||||
|
|
||||||
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
|
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ from twisted.conch.interfaces import IConchUser
|
||||||
_SSH_IMPORT_ERROR = """
|
_SSH_IMPORT_ERROR = """
|
||||||
ERROR: Missing crypto library for SSH. Install it with
|
ERROR: Missing crypto library for SSH. Install it with
|
||||||
|
|
||||||
pip install cryptography
|
pip install cryptography pyasn1
|
||||||
|
|
||||||
(On older Twisted versions you may have to do 'pip install pycrypto pyasn1 instead).
|
(On older Twisted versions you may have to do 'pip install pycrypto pyasn1' instead).
|
||||||
|
|
||||||
If you get a compilation error you must install a C compiler and the
|
If you get a compilation error you must install a C compiler and the
|
||||||
SSL dev headers (On Debian-derived systems this is the gcc and libssl-dev
|
SSL dev headers (On Debian-derived systems this is the gcc and libssl-dev
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
# when it reaches 0 the portal/server syncs their data
|
# when it reaches 0 the portal/server syncs their data
|
||||||
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
||||||
self.init_session(self.protocol_name, client_address, self.factory.sessionhandler)
|
self.init_session(self.protocol_name, client_address, self.factory.sessionhandler)
|
||||||
|
|
||||||
|
self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8'
|
||||||
# add this new connection to sessionhandler so
|
# add this new connection to sessionhandler so
|
||||||
# the Server becomes aware of it.
|
# the Server becomes aware of it.
|
||||||
self.sessionhandler.connect(self)
|
self.sessionhandler.connect(self)
|
||||||
|
# change encoding to ENCODINGS[0] which reflects Telnet default encoding
|
||||||
|
|
||||||
# suppress go-ahead
|
# suppress go-ahead
|
||||||
self.sga = suppress_ga.SuppressGA(self)
|
self.sga = suppress_ga.SuppressGA(self)
|
||||||
|
|
|
||||||
|
|
@ -546,7 +546,7 @@ if AMP_ENABLED:
|
||||||
ifacestr = ""
|
ifacestr = ""
|
||||||
if AMP_INTERFACE != '127.0.0.1':
|
if AMP_INTERFACE != '127.0.0.1':
|
||||||
ifacestr = "-%s" % AMP_INTERFACE
|
ifacestr = "-%s" % AMP_INTERFACE
|
||||||
print(' amp (to Portal)%s: %s' % (ifacestr, AMP_PORT))
|
print(' amp (to Portal)%s: %s (internal)' % (ifacestr, AMP_PORT))
|
||||||
|
|
||||||
from evennia.server import amp
|
from evennia.server import amp
|
||||||
|
|
||||||
|
|
@ -586,7 +586,7 @@ if WEBSERVER_ENABLED:
|
||||||
webserver.setName('EvenniaWebServer%s' % serverport)
|
webserver.setName('EvenniaWebServer%s' % serverport)
|
||||||
EVENNIA.services.addService(webserver)
|
EVENNIA.services.addService(webserver)
|
||||||
|
|
||||||
print(" webserver: %s" % serverport)
|
print(" webserver: %s (internal)" % serverport)
|
||||||
|
|
||||||
ENABLED = []
|
ENABLED = []
|
||||||
if IRC_ENABLED:
|
if IRC_ENABLED:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from builtins import object
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
#------------------------------------------------------------
|
||||||
# Server Session
|
# Server Session
|
||||||
#------------------------------------------------------------
|
#------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -83,15 +83,20 @@ WEBCLIENT_ENABLED = True
|
||||||
# default webclient will use this and only use the ajax version if the browser
|
# default webclient will use this and only use the ajax version if the browser
|
||||||
# is too old to support websockets. Requires WEBCLIENT_ENABLED.
|
# is too old to support websockets. Requires WEBCLIENT_ENABLED.
|
||||||
WEBSOCKET_CLIENT_ENABLED = True
|
WEBSOCKET_CLIENT_ENABLED = True
|
||||||
# Server-side websocket port to open for the webclient.
|
# Server-side websocket port to open for the webclient. Note that this value will
|
||||||
|
# be dynamically encoded in the webclient html page to allow the webclient to call
|
||||||
|
# home. If the external encoded value needs to be different than this, due to
|
||||||
|
# working through a proxy or docker port-remapping, the environment variable
|
||||||
|
# WEBCLIENT_CLIENT_PROXY_PORT can be used to override this port only for the
|
||||||
|
# front-facing client's sake.
|
||||||
WEBSOCKET_CLIENT_PORT = 4005
|
WEBSOCKET_CLIENT_PORT = 4005
|
||||||
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
||||||
WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
|
WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
|
||||||
# Actual URL for webclient component to reach the websocket. You only need
|
# Actual URL for webclient component to reach the websocket. You only need
|
||||||
# to set this if you know you need it, like using some sort of proxy setup.
|
# to set this if you know you need it, like using some sort of proxy setup.
|
||||||
# If given it must be on the form "ws://hostname" (WEBSOCKET_CLIENT_PORT will
|
# If given it must be on the form "ws[s]://hostname[:port]". If left at None,
|
||||||
# be automatically appended). If left at None, the client will itself
|
# the client will itself figure out this url based on the server's hostname.
|
||||||
# figure out this url based on the server's hostname.
|
# e.g. ws://external.example.com or wss://external.example.com:443
|
||||||
WEBSOCKET_CLIENT_URL = None
|
WEBSOCKET_CLIENT_URL = None
|
||||||
# This determine's whether Evennia's custom admin page is used, or if the
|
# This determine's whether Evennia's custom admin page is used, or if the
|
||||||
# standard Django admin is used.
|
# standard Django admin is used.
|
||||||
|
|
@ -166,6 +171,7 @@ IDLE_COMMAND = "idle"
|
||||||
# given, this list is tried, in order, aborting on the first match.
|
# given, this list is tried, in order, aborting on the first match.
|
||||||
# Add sets for languages/regions your accounts are likely to use.
|
# Add sets for languages/regions your accounts are likely to use.
|
||||||
# (see http://en.wikipedia.org/wiki/Character_encoding)
|
# (see http://en.wikipedia.org/wiki/Character_encoding)
|
||||||
|
# Telnet default encoding, unless specified by the client, will be ENCODINGS[0].
|
||||||
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
||||||
# Regular expression applied to all output to a given session in order
|
# Regular expression applied to all output to a given session in order
|
||||||
# to strip away characters (usually various forms of decorations) for the benefit
|
# to strip away characters (usually various forms of decorations) for the benefit
|
||||||
|
|
|
||||||
|
|
@ -63,23 +63,31 @@ menu is immediately exited and the default "look" command is called.
|
||||||
text (str, tuple or None): Text shown at this node. If a tuple, the
|
text (str, tuple or None): Text shown at this node. If a tuple, the
|
||||||
second element in the tuple is a help text to display at this
|
second element in the tuple is a help text to display at this
|
||||||
node when the user enters the menu help command there.
|
node when the user enters the menu help command there.
|
||||||
options (tuple, dict or None): (
|
options (tuple, dict or None): If `None`, this exits the menu.
|
||||||
{'key': name, # can also be a list of aliases. A special key is
|
If a single dict, this is a single-option node. If a tuple,
|
||||||
# "_default", which marks this option as the default
|
it should be a tuple of option dictionaries. Option dicts have
|
||||||
# fallback when no other option matches the user input.
|
the following keys:
|
||||||
'desc': description, # optional description
|
- `key` (str or tuple, optional): What to enter to choose this option.
|
||||||
'goto': nodekey, # node to go to when chosen. This can also be a callable with
|
If a tuple, it must be a tuple of strings, where the first string is the
|
||||||
# caller and/or raw_string args. It must return a string
|
key which will be shown to the user and the others are aliases.
|
||||||
# with the key pointing to the node to go to.
|
If unset, the options' number will be used. The special key `_default`
|
||||||
'exec': nodekey}, # node or callback to trigger as callback when chosen. This
|
marks this option as the default fallback when no other option matches
|
||||||
# will execute *before* going to the next node. Both node
|
the user input. There can only be one `_default` option per node. It
|
||||||
# and the explicit callback will be called as normal nodes
|
will not be displayed in the list.
|
||||||
# (with caller and/or raw_string args). If the callable/node
|
- `desc` (str, optional): This describes what choosing the option will do.
|
||||||
# returns a single string (only), this will replace the current
|
- `goto` (str, tuple or callable): If string, should be the name of node to go to
|
||||||
# goto location string in-place (if a goto callback, it will never fire).
|
when this option is selected. If a callable, it has the signature
|
||||||
# Note that relying to much on letting exec assign the goto
|
`callable(caller[,raw_input][,**kwargs]). If a tuple, the first element
|
||||||
# location can make it hard to debug your menu logic.
|
is the callable and the second is a dict with the **kwargs to pass to
|
||||||
{...}, ...)
|
the callable. Those kwargs will also be passed into the next node if possible.
|
||||||
|
Such a callable should return either a str or a (str, dict), where the
|
||||||
|
string is the name of the next node to go to and the dict is the new,
|
||||||
|
(possibly modified) kwarg to pass into the next node.
|
||||||
|
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
||||||
|
and runs before it. If given a node name, the node will be executed but will not
|
||||||
|
be considered the next node. If node/callback returns str or (str, dict), these will
|
||||||
|
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
||||||
|
next node name and the optional dict acting as the kwargs-input for the next node.
|
||||||
|
|
||||||
If key is not given, the option will automatically be identified by
|
If key is not given, the option will automatically be identified by
|
||||||
its number 1..N.
|
its number 1..N.
|
||||||
|
|
@ -95,7 +103,7 @@ Example:
|
||||||
"This is help text for this node")
|
"This is help text for this node")
|
||||||
options = ({"key": "testing",
|
options = ({"key": "testing",
|
||||||
"desc": "Select this to go to node 2",
|
"desc": "Select this to go to node 2",
|
||||||
"goto": "node2",
|
"goto": ("node2", {"foo": "bar"}),
|
||||||
"exec": "callback1"},
|
"exec": "callback1"},
|
||||||
{"desc": "Go to node 3.",
|
{"desc": "Go to node 3.",
|
||||||
"goto": "node3"})
|
"goto": "node3"})
|
||||||
|
|
@ -108,12 +116,13 @@ Example:
|
||||||
# by the normal 'goto' option key above.
|
# by the normal 'goto' option key above.
|
||||||
caller.msg("Callback called!")
|
caller.msg("Callback called!")
|
||||||
|
|
||||||
def node2(caller):
|
def node2(caller, **kwargs):
|
||||||
text = '''
|
text = '''
|
||||||
This is node 2. It only allows you to go back
|
This is node 2. It only allows you to go back
|
||||||
to the original node1. This extra indent will
|
to the original node1. This extra indent will
|
||||||
be stripped. We don't include a help text.
|
be stripped. We don't include a help text but
|
||||||
'''
|
here are the variables passed to us: {}
|
||||||
|
'''.format(kwargs)
|
||||||
options = {"goto": "node1"}
|
options = {"goto": "node1"}
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
@ -148,6 +157,7 @@ evennia.utils.evmenu`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
import random
|
||||||
from builtins import object, range
|
from builtins import object, range
|
||||||
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
@ -260,7 +270,7 @@ class CmdEvMenuNode(Command):
|
||||||
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
|
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
|
||||||
orig_caller.msg(err) # don't give the session as a kwarg here, direct to original
|
orig_caller.msg(err) # don't give the session as a kwarg here, direct to original
|
||||||
raise EvMenuError(err)
|
raise EvMenuError(err)
|
||||||
# we must do this after the caller with the menui has been correctly identified since it
|
# we must do this after the caller with the menu has been correctly identified since it
|
||||||
# can be either Account, Object or Session (in the latter case this info will be superfluous).
|
# can be either Account, Object or Session (in the latter case this info will be superfluous).
|
||||||
caller.ndb._menutree._session = self.session
|
caller.ndb._menutree._session = self.session
|
||||||
# we have a menu, use it.
|
# we have a menu, use it.
|
||||||
|
|
@ -359,9 +369,10 @@ class EvMenu(object):
|
||||||
re-run with the same input arguments - so be careful if you are counting
|
re-run with the same input arguments - so be careful if you are counting
|
||||||
up some persistent counter or similar - the counter may be run twice if
|
up some persistent counter or similar - the counter may be run twice if
|
||||||
reload happens on the node that does that.
|
reload happens on the node that does that.
|
||||||
startnode_input (str, optional): Send an input text to `startnode` as if
|
startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
|
||||||
a user input text from a fictional previous node. When the server reloads,
|
a user input text from a fictional previous node. If including the dict, this will
|
||||||
the latest visited node will be re-run using this kwarg.
|
be passed as **kwargs to that node. When the server reloads,
|
||||||
|
the latest visited node will be re-run as `node(caller, raw_string, **kwargs)`.
|
||||||
session (Session, optional): This is useful when calling EvMenu from an account
|
session (Session, optional): This is useful when calling EvMenu from an account
|
||||||
in multisession mode > 2. Note that this session only really relevant
|
in multisession mode > 2. Note that this session only really relevant
|
||||||
for the very first display of the first node - after that, EvMenu itself
|
for the very first display of the first node - after that, EvMenu itself
|
||||||
|
|
@ -391,6 +402,7 @@ class EvMenu(object):
|
||||||
self._startnode = startnode
|
self._startnode = startnode
|
||||||
self._menutree = self._parse_menudata(menudata)
|
self._menutree = self._parse_menudata(menudata)
|
||||||
self._persistent = persistent
|
self._persistent = persistent
|
||||||
|
self._quitting = False
|
||||||
|
|
||||||
if startnode not in self._menutree:
|
if startnode not in self._menutree:
|
||||||
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
|
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
|
||||||
|
|
@ -417,6 +429,12 @@ class EvMenu(object):
|
||||||
self.nodetext = None
|
self.nodetext = None
|
||||||
self.helptext = None
|
self.helptext = None
|
||||||
self.options = None
|
self.options = None
|
||||||
|
self.nodename = None
|
||||||
|
self.node_kwargs = {}
|
||||||
|
|
||||||
|
# used for testing
|
||||||
|
self.test_options = {}
|
||||||
|
self.test_nodetext = ""
|
||||||
|
|
||||||
# assign kwargs as initialization vars on ourselves.
|
# assign kwargs as initialization vars on ourselves.
|
||||||
if set(("_startnode", "_menutree", "_session", "_persistent",
|
if set(("_startnode", "_menutree", "_session", "_persistent",
|
||||||
|
|
@ -463,8 +481,13 @@ class EvMenu(object):
|
||||||
menu_cmdset.priority = int(cmdset_priority)
|
menu_cmdset.priority = int(cmdset_priority)
|
||||||
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
||||||
|
|
||||||
|
startnode_kwargs = {}
|
||||||
|
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
|
||||||
|
startnode_input, startnode_kwargs = startnode_input[:2]
|
||||||
|
if not isinstance(startnode_kwargs, dict):
|
||||||
|
raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).")
|
||||||
# start the menu
|
# start the menu
|
||||||
self.goto(self._startnode, startnode_input)
|
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
||||||
|
|
||||||
def _parse_menudata(self, menudata):
|
def _parse_menudata(self, menudata):
|
||||||
"""
|
"""
|
||||||
|
|
@ -519,7 +542,42 @@ class EvMenu(object):
|
||||||
# format the entire node
|
# format the entire node
|
||||||
return self.node_formatter(nodetext, optionstext)
|
return self.node_formatter(nodetext, optionstext)
|
||||||
|
|
||||||
def _execute_node(self, nodename, raw_string):
|
def _safe_call(self, callback, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of
|
||||||
|
which should work also if not present (only `caller` is always required). Return its result.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
nargs = len(getargspec(callback).args)
|
||||||
|
except TypeError:
|
||||||
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
|
supports_kwargs = bool(getargspec(callback).keywords)
|
||||||
|
if nargs <= 0:
|
||||||
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
|
|
||||||
|
if supports_kwargs:
|
||||||
|
if nargs > 1:
|
||||||
|
ret = callback(self.caller, raw_string, **kwargs)
|
||||||
|
# callback accepting raw_string, **kwargs
|
||||||
|
else:
|
||||||
|
# callback accepting **kwargs
|
||||||
|
ret = callback(self.caller, **kwargs)
|
||||||
|
elif nargs > 1:
|
||||||
|
# callback accepting raw_string
|
||||||
|
ret = callback(self.caller, raw_string)
|
||||||
|
else:
|
||||||
|
# normal callback, only the caller as arg
|
||||||
|
ret = callback(self.caller)
|
||||||
|
except EvMenuError:
|
||||||
|
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||||
|
self.caller.msg(errmsg, self._session)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _execute_node(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Execute a node.
|
Execute a node.
|
||||||
|
|
||||||
|
|
@ -528,6 +586,7 @@ class EvMenu(object):
|
||||||
raw_string (str): The raw default string entered on the
|
raw_string (str): The raw default string entered on the
|
||||||
previous node (only used if the node accepts it as an
|
previous node (only used if the node accepts it as an
|
||||||
argument)
|
argument)
|
||||||
|
kwargs (any, optional): Optional kwargs for the node.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
nodetext, options (tuple): The node text (a string or a
|
nodetext, options (tuple): The node text (a string or a
|
||||||
|
|
@ -540,47 +599,25 @@ class EvMenu(object):
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
try:
|
try:
|
||||||
# the node should return data as (text, options)
|
ret = self._safe_call(node, raw_string, **kwargs)
|
||||||
if len(getargspec(node).args) > 1:
|
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||||
# a node accepting raw_string
|
nodetext, options = ret[:2]
|
||||||
nodetext, options = node(self.caller, raw_string)
|
|
||||||
else:
|
else:
|
||||||
# a normal node, only accepting caller
|
nodetext, options = ret, None
|
||||||
nodetext, options = node(self.caller)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
except Exception:
|
except Exception:
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# store options to make them easier to test
|
||||||
|
self.test_options = options
|
||||||
|
self.test_nodetext = nodetext
|
||||||
|
|
||||||
return nodetext, options
|
return nodetext, options
|
||||||
|
|
||||||
def display_nodetext(self):
|
def run_exec(self, nodename, raw_string, **kwargs):
|
||||||
self.caller.msg(self.nodetext, session=self._session)
|
|
||||||
|
|
||||||
def display_helptext(self):
|
|
||||||
self.caller.msg(self.helptext, session=self._session)
|
|
||||||
|
|
||||||
def callback_goto(self, callback, goto, raw_string):
|
|
||||||
"""
|
|
||||||
Call callback and goto in sequence.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
callback (callable or str): Callback to run before goto. If
|
|
||||||
the callback returns a string, this is used to replace
|
|
||||||
the `goto` string before going to the next node.
|
|
||||||
goto (str): The target node to go to next (unless replaced
|
|
||||||
by `callable`)..
|
|
||||||
raw_string (str): The original user input.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if callback:
|
|
||||||
# replace goto only if callback returns
|
|
||||||
goto = self.callback(callback, raw_string) or goto
|
|
||||||
if goto:
|
|
||||||
self.goto(goto, raw_string)
|
|
||||||
|
|
||||||
def callback(self, nodename, raw_string):
|
|
||||||
"""
|
"""
|
||||||
Run a function or node as a callback (with the 'exec' option key).
|
Run a function or node as a callback (with the 'exec' option key).
|
||||||
|
|
||||||
|
|
@ -592,6 +629,8 @@ class EvMenu(object):
|
||||||
raw_string (str): The raw default string entered on the
|
raw_string (str): The raw default string entered on the
|
||||||
previous node (only used if the node accepts it as an
|
previous node (only used if the node accepts it as an
|
||||||
argument)
|
argument)
|
||||||
|
kwargs (any): These are optional kwargs passed into goto
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
new_goto (str or None): A replacement goto location string or
|
new_goto (str or None): A replacement goto location string or
|
||||||
None (no replacement).
|
None (no replacement).
|
||||||
|
|
@ -602,36 +641,36 @@ class EvMenu(object):
|
||||||
relying on this.
|
relying on this.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# this is a direct callable - execute it directly
|
# this is a direct callable - execute it directly
|
||||||
try:
|
ret = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
if len(getargspec(nodename).args) > 1:
|
if isinstance(ret, (tuple, list)):
|
||||||
# callable accepting raw_string
|
if not len(ret) > 1 or not isinstance(ret[1], dict):
|
||||||
ret = nodename(self.caller, raw_string)
|
raise EvMenuError("exec callable must return either None, str or (str, dict)")
|
||||||
|
ret, kwargs = ret[:2]
|
||||||
else:
|
else:
|
||||||
# normal callable, only the caller as arg
|
# nodename is a string; lookup as node and run as node in-place (don't goto it)
|
||||||
ret = nodename(self.caller)
|
|
||||||
except Exception:
|
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# nodename is a string; lookup as node
|
|
||||||
try:
|
|
||||||
# execute the node
|
# execute the node
|
||||||
ret = self._execute_node(nodename, raw_string)
|
ret = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
|
if isinstance(ret, (tuple, list)):
|
||||||
|
if not len(ret) > 1 and ret[1] and not isinstance(ret[1], dict):
|
||||||
|
raise EvMenuError("exec node must return either None, str or (str, dict)")
|
||||||
|
ret, kwargs = ret[:2]
|
||||||
except EvMenuError as err:
|
except EvMenuError as err:
|
||||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err)
|
||||||
self.caller.msg("|r%s|n" % errmsg)
|
self.caller.msg("|r%s|n" % errmsg)
|
||||||
logger.log_trace(errmsg)
|
logger.log_trace(errmsg)
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(ret, basestring):
|
if isinstance(ret, basestring):
|
||||||
# only return a value if a string (a goto target), ignore all other returns
|
# only return a value if a string (a goto target), ignore all other returns
|
||||||
return ret
|
return ret, kwargs
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def goto(self, nodename, raw_string):
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Run a node by name
|
Run a node by name, optionally dynamically generating that name first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodename (str or callable): Name of node or a callable
|
nodename (str or callable): Name of node or a callable
|
||||||
|
|
@ -642,24 +681,48 @@ class EvMenu(object):
|
||||||
argument)
|
argument)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if callable(nodename):
|
def _extract_goto_exec(option_dict):
|
||||||
try:
|
"Helper: Get callables and their eventual kwargs"
|
||||||
if len(getargspec(nodename).args) > 1:
|
goto_kwargs, exec_kwargs = {}, {}
|
||||||
# callable accepting raw_string
|
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||||
nodename = nodename(self.caller, raw_string)
|
if goto and isinstance(goto, (tuple, list)):
|
||||||
|
if len(goto) > 1:
|
||||||
|
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(goto_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
else:
|
else:
|
||||||
nodename = nodename(self.caller)
|
goto = goto[0]
|
||||||
except Exception:
|
if execute and isinstance(execute, (tuple, list)):
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
if len(execute) > 1:
|
||||||
raise
|
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(exec_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
|
else:
|
||||||
|
execute = execute[0]
|
||||||
|
return goto, goto_kwargs, execute, exec_kwargs
|
||||||
|
|
||||||
|
if callable(nodename):
|
||||||
|
# run the "goto" callable, if possible
|
||||||
|
inp_nodename = nodename
|
||||||
|
nodename = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
|
if isinstance(nodename, (tuple, list)):
|
||||||
|
if not len(nodename) > 1 or not isinstance(nodename[1], dict):
|
||||||
|
raise EvMenuError(
|
||||||
|
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||||
|
nodename, kwargs = nodename[:2]
|
||||||
try:
|
try:
|
||||||
# execute the node, make use of the returns.
|
# execute the found node, make use of the returns.
|
||||||
nodetext, options = self._execute_node(nodename, raw_string)
|
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
except EvMenuError:
|
except EvMenuError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._persistent:
|
if self._persistent:
|
||||||
self.caller.attributes.add("_menutree_saved_startnode", (nodename, raw_string))
|
self.caller.attributes.add("_menutree_saved_startnode",
|
||||||
|
(nodename, (raw_string, kwargs)))
|
||||||
|
|
||||||
# validation of the node return values
|
# validation of the node return values
|
||||||
helptext = ""
|
helptext = ""
|
||||||
|
|
@ -680,22 +743,25 @@ class EvMenu(object):
|
||||||
for inum, dic in enumerate(options):
|
for inum, dic in enumerate(options):
|
||||||
# fix up the option dicts
|
# fix up the option dicts
|
||||||
keys = make_iter(dic.get("key"))
|
keys = make_iter(dic.get("key"))
|
||||||
|
desc = dic.get("desc", dic.get("text", None))
|
||||||
if "_default" in keys:
|
if "_default" in keys:
|
||||||
keys = [key for key in keys if key != "_default"]
|
keys = [key for key in keys if key != "_default"]
|
||||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
||||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||||
self.default = (goto, execute)
|
|
||||||
else:
|
else:
|
||||||
|
# use the key (only) if set, otherwise use the running number
|
||||||
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
||||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
||||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
|
||||||
if keys:
|
if keys:
|
||||||
display_options.append((keys[0], desc))
|
display_options.append((keys[0], desc))
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if goto or execute:
|
if goto or execute:
|
||||||
self.options[strip_ansi(key).strip().lower()] = (goto, execute)
|
self.options[strip_ansi(key).strip().lower()] = \
|
||||||
|
(goto, goto_kwargs, execute, exec_kwargs)
|
||||||
|
|
||||||
self.nodetext = self._format_node(nodetext, display_options)
|
self.nodetext = self._format_node(nodetext, display_options)
|
||||||
|
self.node_kwargs = kwargs
|
||||||
|
self.nodename = nodename
|
||||||
|
|
||||||
# handle the helptext
|
# handle the helptext
|
||||||
if helptext:
|
if helptext:
|
||||||
|
|
@ -709,10 +775,37 @@ class EvMenu(object):
|
||||||
if not options:
|
if not options:
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
|
|
||||||
|
def run_exec_then_goto(self, runexec, goto, raw_string, runexec_kwargs=None, goto_kwargs=None):
|
||||||
|
"""
|
||||||
|
Call 'exec' callback and goto (which may also be a callable) in sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
runexec (callable or str): Callback to run before goto. If
|
||||||
|
the callback returns a string, this is used to replace
|
||||||
|
the `goto` string/callable before being passed into the goto handler.
|
||||||
|
goto (str): The target node to go to next (may be replaced
|
||||||
|
by `runexec`)..
|
||||||
|
raw_string (str): The original user input.
|
||||||
|
runexec_kwargs (dict, optional): Optional kwargs for runexec.
|
||||||
|
goto_kwargs (dict, optional): Optional kwargs for goto.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if runexec:
|
||||||
|
# replace goto only if callback returns
|
||||||
|
goto, goto_kwargs = (
|
||||||
|
self.run_exec(runexec, raw_string,
|
||||||
|
**(runexec_kwargs if runexec_kwargs else {})) or
|
||||||
|
(goto, goto_kwargs))
|
||||||
|
if goto:
|
||||||
|
self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {}))
|
||||||
|
|
||||||
def close_menu(self):
|
def close_menu(self):
|
||||||
"""
|
"""
|
||||||
Shutdown menu; occurs when reaching the end node or using the quit command.
|
Shutdown menu; occurs when reaching the end node or using the quit command.
|
||||||
"""
|
"""
|
||||||
|
if not self._quitting:
|
||||||
|
# avoid multiple calls from different sources
|
||||||
|
self._quitting = True
|
||||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||||
del self.caller.ndb._menutree
|
del self.caller.ndb._menutree
|
||||||
if self._persistent:
|
if self._persistent:
|
||||||
|
|
@ -734,13 +827,13 @@ class EvMenu(object):
|
||||||
should also report errors directly to the user.
|
should also report errors directly to the user.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cmd = raw_string.strip().lower()
|
cmd = strip_ansi(raw_string.strip().lower())
|
||||||
|
|
||||||
if cmd in self.options:
|
if cmd in self.options:
|
||||||
# this will take precedence over the default commands
|
# this will take precedence over the default commands
|
||||||
# below
|
# below
|
||||||
goto, callback = self.options[cmd]
|
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||||
self.callback_goto(callback, goto, raw_string)
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
elif self.auto_look and cmd in ("look", "l"):
|
elif self.auto_look and cmd in ("look", "l"):
|
||||||
self.display_nodetext()
|
self.display_nodetext()
|
||||||
elif self.auto_help and cmd in ("help", "h"):
|
elif self.auto_help and cmd in ("help", "h"):
|
||||||
|
|
@ -748,8 +841,8 @@ class EvMenu(object):
|
||||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
elif self.default:
|
elif self.default:
|
||||||
goto, callback = self.default
|
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||||
self.callback_goto(callback, goto, raw_string)
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
else:
|
else:
|
||||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
||||||
|
|
||||||
|
|
@ -757,6 +850,12 @@ class EvMenu(object):
|
||||||
# no options - we are at the end of the menu.
|
# no options - we are at the end of the menu.
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
|
|
||||||
|
def display_nodetext(self):
|
||||||
|
self.caller.msg(self.nodetext, session=self._session)
|
||||||
|
|
||||||
|
def display_helptext(self):
|
||||||
|
self.caller.msg(self.helptext, session=self._session)
|
||||||
|
|
||||||
# formatters - override in a child class
|
# formatters - override in a child class
|
||||||
|
|
||||||
def nodetext_formatter(self, nodetext):
|
def nodetext_formatter(self, nodetext):
|
||||||
|
|
@ -799,16 +898,17 @@ class EvMenu(object):
|
||||||
for key, desc in optionlist:
|
for key, desc in optionlist:
|
||||||
if not (key or desc):
|
if not (key or desc):
|
||||||
continue
|
continue
|
||||||
|
desc_string = ": %s" % desc if desc else ""
|
||||||
table_width_max = max(table_width_max,
|
table_width_max = max(table_width_max,
|
||||||
max(m_len(p) for p in key.split("\n")) +
|
max(m_len(p) for p in key.split("\n")) +
|
||||||
max(m_len(p) for p in desc.split("\n")) + colsep)
|
max(m_len(p) for p in desc_string.split("\n")) + colsep)
|
||||||
raw_key = strip_ansi(key)
|
raw_key = strip_ansi(key)
|
||||||
if raw_key != key:
|
if raw_key != key:
|
||||||
# already decorations in key definition
|
# already decorations in key definition
|
||||||
table.append(" |lc%s|lt%s|le: %s" % (raw_key, key, desc))
|
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
|
||||||
else:
|
else:
|
||||||
# add a default white color to key
|
# add a default white color to key
|
||||||
table.append(" |lc%s|lt|w%s|n|le: %s" % (raw_key, raw_key, desc))
|
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
|
||||||
|
|
||||||
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
|
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
|
||||||
|
|
||||||
|
|
@ -996,6 +1096,10 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
||||||
#
|
#
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
def _generate_goto(caller, **kwargs):
|
||||||
|
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||||
|
|
||||||
|
|
||||||
def test_start_node(caller):
|
def test_start_node(caller):
|
||||||
menu = caller.ndb._menutree
|
menu = caller.ndb._menutree
|
||||||
text = """
|
text = """
|
||||||
|
|
@ -1020,6 +1124,9 @@ def test_start_node(caller):
|
||||||
{"key": ("|yV|niew", "v"),
|
{"key": ("|yV|niew", "v"),
|
||||||
"desc": "View your own name",
|
"desc": "View your own name",
|
||||||
"goto": "test_view_node"},
|
"goto": "test_view_node"},
|
||||||
|
{"key": ("|yD|nynamic", "d"),
|
||||||
|
"desc": "Dynamic node",
|
||||||
|
"goto": (_generate_goto, {"name": "test_dynamic_node"})},
|
||||||
{"key": ("|yQ|nuit", "quit", "q", "Q"),
|
{"key": ("|yQ|nuit", "quit", "q", "Q"),
|
||||||
"desc": "Quit this menu example.",
|
"desc": "Quit this menu example.",
|
||||||
"goto": "test_end_node"},
|
"goto": "test_end_node"},
|
||||||
|
|
@ -1029,7 +1136,7 @@ def test_start_node(caller):
|
||||||
|
|
||||||
|
|
||||||
def test_look_node(caller):
|
def test_look_node(caller):
|
||||||
text = ""
|
text = "This is a custom look location!"
|
||||||
options = {"key": ("|yL|nook", "l"),
|
options = {"key": ("|yL|nook", "l"),
|
||||||
"desc": "Go back to the previous menu.",
|
"desc": "Go back to the previous menu.",
|
||||||
"goto": "test_start_node"}
|
"goto": "test_start_node"}
|
||||||
|
|
@ -1056,12 +1163,11 @@ def test_set_node(caller):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
options = {"key": ("back (default)", "_default"),
|
options = {"key": ("back (default)", "_default"),
|
||||||
"desc": "back to main",
|
|
||||||
"goto": "test_start_node"}
|
"goto": "test_start_node"}
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_view_node(caller):
|
def test_view_node(caller, **kwargs):
|
||||||
text = """
|
text = """
|
||||||
Your name is |g%s|n!
|
Your name is |g%s|n!
|
||||||
|
|
||||||
|
|
@ -1071,6 +1177,11 @@ def test_view_node(caller):
|
||||||
-always- use numbers (1...N) to refer to listed options also if you
|
-always- use numbers (1...N) to refer to listed options also if you
|
||||||
don't see a string option key (try it!).
|
don't see a string option key (try it!).
|
||||||
""" % caller.key
|
""" % caller.key
|
||||||
|
if kwargs.get("executed_from_dynamic_node", False):
|
||||||
|
# we are calling this node as a exec, skip return values
|
||||||
|
caller.msg("|gCalled from dynamic node:|n \n {}".format(text))
|
||||||
|
return
|
||||||
|
else:
|
||||||
options = {"desc": "back to main",
|
options = {"desc": "back to main",
|
||||||
"goto": "test_start_node"}
|
"goto": "test_start_node"}
|
||||||
return text, options
|
return text, options
|
||||||
|
|
@ -1089,12 +1200,48 @@ def test_displayinput_node(caller, raw_string):
|
||||||
makes it hidden from view. It catches all input (except the
|
makes it hidden from view. It catches all input (except the
|
||||||
in-menu help/quit commands) and will, in this case, bring you back
|
in-menu help/quit commands) and will, in this case, bring you back
|
||||||
to the start node.
|
to the start node.
|
||||||
""" % raw_string
|
""" % raw_string.rstrip()
|
||||||
options = {"key": "_default",
|
options = {"key": "_default",
|
||||||
"goto": "test_start_node"}
|
"goto": "test_start_node"}
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
def _test_call(caller, raw_input, **kwargs):
|
||||||
|
mode = kwargs.get("mode", "exec")
|
||||||
|
|
||||||
|
caller.msg("\n|y'{}' |n_test_call|y function called with\n "
|
||||||
|
"caller: |n{}\n |yraw_input: \"|n{}|y\" \n kwargs: |n{}\n".format(
|
||||||
|
mode, caller, raw_input.rstrip(), kwargs))
|
||||||
|
|
||||||
|
if mode == "exec":
|
||||||
|
kwargs = {"random": random.random()}
|
||||||
|
caller.msg("function modify kwargs to {}".format(kwargs))
|
||||||
|
else:
|
||||||
|
caller.msg("|ypassing function kwargs without modification.|n")
|
||||||
|
|
||||||
|
return "test_dynamic_node", kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_node(caller, **kwargs):
|
||||||
|
text = """
|
||||||
|
This is a dynamic node with input:
|
||||||
|
{}
|
||||||
|
""".format(kwargs)
|
||||||
|
options = ({"desc": "pass a new random number to this node",
|
||||||
|
"goto": ("test_dynamic_node", {"random": random.random()})},
|
||||||
|
{"desc": "execute a func with kwargs",
|
||||||
|
"exec": (_test_call, {"mode": "exec", "test_random": random.random()})},
|
||||||
|
{"desc": "dynamic_goto",
|
||||||
|
"goto": (_test_call, {"mode": "goto", "goto_input": "test"})},
|
||||||
|
{"desc": "exec test_view_node with kwargs",
|
||||||
|
"exec": ("test_view_node", {"executed_from_dynamic_node": True}),
|
||||||
|
"goto": "test_dynamic_node"},
|
||||||
|
{"desc": "back to main",
|
||||||
|
"goto": "test_start_node"})
|
||||||
|
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_end_node(caller):
|
def test_end_node(caller):
|
||||||
text = """
|
text = """
|
||||||
This is the end of the menu and since it has no options the menu
|
This is the end of the menu and since it has no options the menu
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ def _batch_create_object(*objparams):
|
||||||
objects (list): A list of created objects
|
objects (list): A list of created objects
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
The `exec` list will execute arbitrary python code so don't allow this to be availble to
|
The `exec` list will execute arbitrary python code so don't allow this to be available to
|
||||||
unprivileged users!
|
unprivileged users!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,227 @@
|
||||||
"""
|
"""
|
||||||
Unit tests for the EvMenu system
|
Unit tests for the EvMenu system
|
||||||
|
|
||||||
TODO: This need expansion.
|
This sets up a testing parent for testing EvMenu trees. It is configured by subclassing the
|
||||||
|
`TestEvMenu` class from this module and setting the class variables to point to the menu that should
|
||||||
|
be tested and how it should be called.
|
||||||
|
|
||||||
|
Without adding any further test methods, the tester will process all nodes of the menu, depth first,
|
||||||
|
by stepping through all options for every node. Optionally, it can check that all nodes are visited.
|
||||||
|
It will create a hierarchical list of node names that describes the tree structure. This can then be
|
||||||
|
compared against a template to make sure the menu structure is sound. Easiest way to use this is to
|
||||||
|
run the test once to see how the structure looks.
|
||||||
|
|
||||||
|
The system also allows for testing the returns of each node as part of the parsing.
|
||||||
|
|
||||||
|
To help debug the menu, turn on `debug_output`, which will print the traversal process in detail.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from evennia.utils import evmenu
|
from evennia.utils import evmenu
|
||||||
from mock import Mock
|
from evennia.utils import ansi
|
||||||
|
from mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
class TestEvMenu(TestCase):
|
class TestEvMenu(TestCase):
|
||||||
"Run the EvMenu testing."
|
"Run the EvMenu testing."
|
||||||
|
menutree = {} # can also be the path to the menu tree
|
||||||
|
startnode = "start"
|
||||||
|
cmdset_mergetype = "Replace"
|
||||||
|
cmdset_priority = 1
|
||||||
|
auto_quit = True
|
||||||
|
auto_look = True
|
||||||
|
auto_help = True
|
||||||
|
cmd_on_exit = "look"
|
||||||
|
persistent = False
|
||||||
|
startnode_input = ""
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
# if all nodes must be visited for the test to pass. This is not on
|
||||||
|
# by default since there may be exec-nodes that are made to not be
|
||||||
|
# visited.
|
||||||
|
expect_all_nodes = False
|
||||||
|
|
||||||
|
# this is compared against the full tree structure generated
|
||||||
|
expected_tree = []
|
||||||
|
# this allows for verifying that a given node returns a given text. The
|
||||||
|
# text is compared with .startswith, so the entire text need not be matched.
|
||||||
|
expected_node_texts = {}
|
||||||
|
# just check the number of options from each node
|
||||||
|
expected_node_options_count = {}
|
||||||
|
# check the actual options
|
||||||
|
expected_node_options = {}
|
||||||
|
|
||||||
|
# set this to print the traversal as it happens (debugging)
|
||||||
|
debug_output = False
|
||||||
|
|
||||||
|
def _debug_output(self, indent, msg):
|
||||||
|
if self.debug_output:
|
||||||
|
print(" " * indent + msg)
|
||||||
|
|
||||||
|
def _test_menutree(self, menu):
|
||||||
|
"""
|
||||||
|
This is a automatic tester of the menu tree by recursively progressing through the
|
||||||
|
structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _depth_first(menu, tree, visited, indent):
|
||||||
|
|
||||||
|
# we are in a given node here
|
||||||
|
nodename = menu.nodename
|
||||||
|
options = menu.test_options
|
||||||
|
if isinstance(options, dict):
|
||||||
|
options = (options, )
|
||||||
|
|
||||||
|
# run validation tests for this node
|
||||||
|
compare_text = self.expected_node_texts.get(nodename, None)
|
||||||
|
if compare_text is not None:
|
||||||
|
compare_text = ansi.strip_ansi(compare_text.strip())
|
||||||
|
node_text = menu.test_nodetext
|
||||||
|
self.assertIsNotNone(
|
||||||
|
bool(node_text),
|
||||||
|
"node: {}: node-text is None, which was not expected.".format(nodename))
|
||||||
|
node_text = ansi.strip_ansi(node_text.strip())
|
||||||
|
self.assertTrue(
|
||||||
|
node_text.startswith(compare_text),
|
||||||
|
"\nnode \"{}\':\nOutput:\n{}\n\nExpected (startswith):\n{}".format(
|
||||||
|
nodename, node_text, compare_text))
|
||||||
|
compare_options_count = self.expected_node_options_count.get(nodename, None)
|
||||||
|
if compare_options_count is not None:
|
||||||
|
self.assertEqual(
|
||||||
|
len(options), compare_options_count,
|
||||||
|
"Not the right number of options returned from node {}.".format(nodename))
|
||||||
|
compare_options = self.expected_node_options.get(nodename, None)
|
||||||
|
if compare_options:
|
||||||
|
self.assertEqual(
|
||||||
|
options, compare_options,
|
||||||
|
"Options returned from node {} does not match.".format(nodename))
|
||||||
|
|
||||||
|
self._debug_output(indent, "*{}".format(nodename))
|
||||||
|
subtree = []
|
||||||
|
|
||||||
|
if not options:
|
||||||
|
# an end node
|
||||||
|
if nodename not in visited:
|
||||||
|
visited.append(nodename)
|
||||||
|
subtree = nodename
|
||||||
|
else:
|
||||||
|
for inum, optdict in enumerate(options):
|
||||||
|
|
||||||
|
key, desc, execute, goto = optdict.get("key", ""), optdict.get("desc", None),\
|
||||||
|
optdict.get("exec", None), optdict.get("goto", None)
|
||||||
|
|
||||||
|
# prepare the key to pass to the menu
|
||||||
|
if isinstance(key, (tuple, list)) and len(key) > 1:
|
||||||
|
key = key[0]
|
||||||
|
if key == "_default":
|
||||||
|
key = "test raw input"
|
||||||
|
if not key:
|
||||||
|
key = str(inum + 1)
|
||||||
|
|
||||||
|
backup_menu = copy.copy(menu)
|
||||||
|
|
||||||
|
# step the menu
|
||||||
|
menu.parse_input(key)
|
||||||
|
|
||||||
|
# from here on we are likely in a different node
|
||||||
|
nodename = menu.nodename
|
||||||
|
|
||||||
|
if menu.close_menu.called:
|
||||||
|
# this was an end node
|
||||||
|
self._debug_output(indent, " .. menu exited! Back to previous node.")
|
||||||
|
menu = backup_menu
|
||||||
|
menu.close_menu = MagicMock()
|
||||||
|
visited.append(nodename)
|
||||||
|
subtree.append(nodename)
|
||||||
|
elif nodename not in visited:
|
||||||
|
visited.append(nodename)
|
||||||
|
subtree.append(nodename)
|
||||||
|
_depth_first(menu, subtree, visited, indent + 2)
|
||||||
|
#self._debug_output(indent, " -> arrived at {}".format(nodename))
|
||||||
|
else:
|
||||||
|
subtree.append(nodename)
|
||||||
|
#self._debug_output( indent, " -> arrived at {} (circular call)".format(nodename))
|
||||||
|
self._debug_output(indent, "-- {} ({}) -> {}".format(key, desc, goto))
|
||||||
|
|
||||||
|
if subtree:
|
||||||
|
tree.append(subtree)
|
||||||
|
|
||||||
|
# the start node has already fired at this point
|
||||||
|
visited_nodes = [menu.nodename]
|
||||||
|
traversal_tree = [menu.nodename]
|
||||||
|
_depth_first(menu, traversal_tree, visited_nodes, 1)
|
||||||
|
|
||||||
|
if self.expect_all_nodes:
|
||||||
|
self.assertGreaterEqual(len(menu._menutree), len(visited_nodes))
|
||||||
|
self.assertEqual(traversal_tree, self.expected_tree)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.caller = Mock()
|
self.menu = None
|
||||||
self.caller.msg = Mock()
|
if self.menutree:
|
||||||
self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node",
|
self.caller = MagicMock()
|
||||||
persistent=True, cmdset_mergetype="Replace", testval="val",
|
self.caller.key = "Test"
|
||||||
testval2="val2")
|
self.caller2 = MagicMock()
|
||||||
|
self.caller2.key = "Test"
|
||||||
|
self.caller.msg = MagicMock()
|
||||||
|
self.caller2.msg = MagicMock()
|
||||||
|
self.session = MagicMock()
|
||||||
|
self.session2 = MagicMock()
|
||||||
|
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
|
||||||
|
cmdset_mergetype=self.cmdset_mergetype,
|
||||||
|
cmdset_priority=self.cmdset_priority,
|
||||||
|
auto_quit=self.auto_quit, auto_look=self.auto_look,
|
||||||
|
auto_help=self.auto_help,
|
||||||
|
cmd_on_exit=self.cmd_on_exit, persistent=False,
|
||||||
|
startnode_input=self.startnode_input, session=self.session,
|
||||||
|
**self.kwargs)
|
||||||
|
# persistent version
|
||||||
|
self.pmenu = evmenu.EvMenu(self.caller2, self.menutree, startnode=self.startnode,
|
||||||
|
cmdset_mergetype=self.cmdset_mergetype,
|
||||||
|
cmdset_priority=self.cmdset_priority,
|
||||||
|
auto_quit=self.auto_quit, auto_look=self.auto_look,
|
||||||
|
auto_help=self.auto_help,
|
||||||
|
cmd_on_exit=self.cmd_on_exit, persistent=True,
|
||||||
|
startnode_input=self.startnode_input, session=self.session2,
|
||||||
|
**self.kwargs)
|
||||||
|
|
||||||
|
self.menu.close_menu = MagicMock()
|
||||||
|
self.pmenu.close_menu = MagicMock()
|
||||||
|
|
||||||
|
def test_menu_structure(self):
|
||||||
|
if self.menu:
|
||||||
|
self._test_menutree(self.menu)
|
||||||
|
self._test_menutree(self.pmenu)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvMenuExample(TestEvMenu):
|
||||||
|
|
||||||
|
menutree = "evennia.utils.evmenu"
|
||||||
|
startnode = "test_start_node"
|
||||||
|
kwargs = {"testval": "val", "testval2": "val2"}
|
||||||
|
debug_output = False
|
||||||
|
|
||||||
|
expected_node_texts = {
|
||||||
|
"test_view_node": "Your name is"}
|
||||||
|
|
||||||
|
expected_tree = \
|
||||||
|
['test_start_node',
|
||||||
|
['test_set_node',
|
||||||
|
['test_start_node'],
|
||||||
|
'test_look_node',
|
||||||
|
['test_start_node'],
|
||||||
|
'test_view_node',
|
||||||
|
['test_start_node'],
|
||||||
|
'test_dynamic_node',
|
||||||
|
['test_dynamic_node',
|
||||||
|
'test_dynamic_node',
|
||||||
|
'test_dynamic_node',
|
||||||
|
'test_dynamic_node',
|
||||||
|
'test_start_node'],
|
||||||
|
'test_end_node',
|
||||||
|
'test_displayinput_node',
|
||||||
|
['test_start_node']]]
|
||||||
|
|
||||||
def test_kwargsave(self):
|
def test_kwargsave(self):
|
||||||
self.assertTrue(hasattr(self.menu, "testval"))
|
self.assertTrue(hasattr(self.menu, "testval"))
|
||||||
|
|
|
||||||
|
|
@ -1786,8 +1786,12 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
||||||
error = kwargs.get("nofound_string") or _("Could not find '%s'." % query)
|
error = kwargs.get("nofound_string") or _("Could not find '%s'." % query)
|
||||||
matches = None
|
matches = None
|
||||||
elif len(matches) > 1:
|
elif len(matches) > 1:
|
||||||
error = kwargs.get("multimatch_string") or \
|
multimatch_string = kwargs.get("multimatch_string")
|
||||||
_("More than one match for '%s' (please narrow target):\n" % query)
|
if multimatch_string:
|
||||||
|
error = "%s\n" % multimatch_string
|
||||||
|
else:
|
||||||
|
error = _("More than one match for '%s' (please narrow target):\n" % query)
|
||||||
|
|
||||||
for num, result in enumerate(matches):
|
for num, result in enumerate(matches):
|
||||||
# we need to consider Commands, where .aliases is a list
|
# we need to consider Commands, where .aliases is a list
|
||||||
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
# tuple.
|
# tuple.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils.utils import get_evennia_version
|
from evennia.utils.utils import get_evennia_version
|
||||||
|
|
||||||
|
|
@ -52,7 +53,11 @@ def set_webclient_settings():
|
||||||
global WEBCLIENT_ENABLED, WEBSOCKET_CLIENT_ENABLED, WEBSOCKET_PORT, WEBSOCKET_URL
|
global WEBCLIENT_ENABLED, WEBSOCKET_CLIENT_ENABLED, WEBSOCKET_PORT, WEBSOCKET_URL
|
||||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||||
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED
|
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED
|
||||||
WEBSOCKET_PORT = settings.WEBSOCKET_CLIENT_PORT
|
# if we are working through a proxy or uses docker port-remapping, the webclient port encoded
|
||||||
|
# in the webclient should be different than the one the server expects. Use the environment
|
||||||
|
# variable WEBSOCKET_CLIENT_PROXY_PORT if this is the case.
|
||||||
|
WEBSOCKET_PORT = int(os.environ.get("WEBSOCKET_CLIENT_PROXY_PORT", settings.WEBSOCKET_CLIENT_PORT))
|
||||||
|
# this is determined dynamically by the client and is less of an issue
|
||||||
WEBSOCKET_URL = settings.WEBSOCKET_CLIENT_URL
|
WEBSOCKET_URL = settings.WEBSOCKET_CLIENT_URL
|
||||||
set_webclient_settings()
|
set_webclient_settings()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ class TestGeneralContext(TestCase):
|
||||||
mock_settings.WEBCLIENT_ENABLED = "webclient"
|
mock_settings.WEBCLIENT_ENABLED = "webclient"
|
||||||
mock_settings.WEBSOCKET_CLIENT_URL = "websocket_url"
|
mock_settings.WEBSOCKET_CLIENT_URL = "websocket_url"
|
||||||
mock_settings.WEBSOCKET_CLIENT_ENABLED = "websocket_client"
|
mock_settings.WEBSOCKET_CLIENT_ENABLED = "websocket_client"
|
||||||
mock_settings.WEBSOCKET_CLIENT_PORT = "websocket_port"
|
mock_settings.WEBSOCKET_CLIENT_PORT = 5000
|
||||||
general_context.set_webclient_settings()
|
general_context.set_webclient_settings()
|
||||||
self.assertEqual(general_context.WEBCLIENT_ENABLED, "webclient")
|
self.assertEqual(general_context.WEBCLIENT_ENABLED, "webclient")
|
||||||
self.assertEqual(general_context.WEBSOCKET_URL, "websocket_url")
|
self.assertEqual(general_context.WEBSOCKET_URL, "websocket_url")
|
||||||
self.assertEqual(general_context.WEBSOCKET_CLIENT_ENABLED, "websocket_client")
|
self.assertEqual(general_context.WEBSOCKET_CLIENT_ENABLED, "websocket_client")
|
||||||
self.assertEqual(general_context.WEBSOCKET_PORT, "websocket_port")
|
self.assertEqual(general_context.WEBSOCKET_PORT, 5000)
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,10 @@ function onNewLine(text, originator) {
|
||||||
document.title = "(" + unread + ") " + originalTitle;
|
document.title = "(" + unread + ") " + originalTitle;
|
||||||
if ("Notification" in window){
|
if ("Notification" in window){
|
||||||
if (("notification_popup" in options) && (options["notification_popup"])) {
|
if (("notification_popup" in options) && (options["notification_popup"])) {
|
||||||
Notification.requestPermission().then(function(result) {
|
// There is a Promise-based API for this, but it’s not supported
|
||||||
|
// in Safari and some older browsers:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission#Browser_compatibility
|
||||||
|
Notification.requestPermission(function(result) {
|
||||||
if(result === "granted") {
|
if(result === "granted") {
|
||||||
var title = originalTitle === "" ? "Evennia" : originalTitle;
|
var title = originalTitle === "" ? "Evennia" : originalTitle;
|
||||||
var options = {
|
var options = {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ JQuery available.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if websocket_url %}
|
{% if websocket_url %}
|
||||||
var wsurl = "{{websocket_url}}:{{websocket_port}}";
|
var wsurl = "{{websocket_url}}";
|
||||||
{% else %}
|
{% else %}
|
||||||
var wsurl = "ws://" + this.location.hostname + ":{{websocket_port}}";
|
var wsurl = "ws://" + this.location.hostname + ":{{websocket_port}}";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue