6. 角色生成¶
在之前的课程中,我们已经确定了角色的样子。现在,我们需要给玩家一个创建角色的机会。
6.1. 工作原理¶
一个全新的Evennia安装在你登录时会自动创建一个与账户同名的新角色。这既快速又简单,模仿了旧的MUD风格。你可以想象这样做,然后在原地自定义角色。
不过,我们会更复杂一些。我们希望用户在登录时能够使用菜单创建角色。
我们通过编辑mygame/server/conf/settings.py
并添加以下行来实现这一点:
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
这样做后,使用新账户连接游戏时会进入“OOC”模式。look
的OOC版本(位于Account cmdset中)会显示可用角色的列表(如果有的话)。你还可以输入charcreate
来创建一个新角色。charcreate
是Evennia自带的一个简单命令,只需让你用给定的名称和描述创建一个新角色。我们稍后会修改它以启动我们的角色生成。现在我们只需记住,这就是我们将如何开始菜单。
在_Knave_中,大多数角色生成是随机的。这意味着本教程可以非常紧凑,同时仍能展示基本思想。我们将创建一个如下所示的菜单:
Silas
STR +1
DEX +2
CON +1
INT +3
WIS +1
CHA +2
你身材瘦长,面孔凹陷,头发肮脏,讲话气喘吁吁,穿着异国服装。
你曾是一个草药师,但你被追捕,最终成为了一个流浪者。你诚实但也多疑。你是中立阵营。
你的物品:
锁子甲,口粮,口粮,剑,火把,火把,火把,火把,火把,火种盒,凿子,哨子
----------------------------------------------------------------------------------------
1. 更改你的名字
2. 交换两个能力值(一次)
3. 接受并创建角色
如果你选择1,你会进入一个新的菜单节点:
你当前的名字是Silas。输入一个新名字或留空以中止。
-----------------------------------------------------------------------------------------
你现在可以输入一个新名字。按下回车后,你将返回到第一个菜单节点,显示你的角色,现在有了新名字。
如果你选择2,你会进入另一个菜单节点:
你当前的能力:
STR +1
DEX +2
CON +1
INT +3
WIS +1
CHA +2
你可以交换两个能力的值。
你只能这样做一次,所以请谨慎选择!
要交换例如STR和INT的值,请输入“STR INT”。留空以中止。
------------------------------------------------------------------------------------------
如果你在这里输入WIS CHA
,WIS将变为+2
,CHA将变为+1
。然后你将再次返回到主节点以查看你的新角色,但这次交换选项将不再可用(你只能这样做一次)。
如果你最终选择“接受并创建角色”选项,角色将被创建,你将离开菜单:
角色已创建!
6.2. 随机表¶
创建一个新模块
mygame/evadventure/random_tables.py
。
由于_Knave_的大多数角色生成是随机的,我们将需要从_Knave_规则书中掷骰随机表。虽然我们在规则教程中添加了在随机表上掷骰的功能,但我们还没有添加相关的表。
# 在 mygame/evadventure/random_tables.py 中
chargen_tables = {
"physique": [
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
],
"face": [
"bloated", "blunt", "bony", # ...
], # ...
}
这些表只是从_Knave_规则中复制的。我们将这些方面分组到一个字典character_generation
中,以将角色生成专用表与我们将在此处保留的其他随机表分开。
6.3. 存储菜单状态¶
创建一个新模块
mygame/evadventure/chargen.py
。
在角色生成过程中,我们将需要一个实体来存储/保留更改,就像一个“临时角色纸”一样。
# 在 mygame/evadventure/chargen.py 中
from .random_tables import chargen_tables
from .rules import dice
class TemporaryCharacterSheet:
def _random_ability(self):
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
def __init__(self):
self.ability_changes = 0 # 我们尝试交换能力的次数
# 名字可能会在以后修改
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
# 基础属性值
self.strength = self._random_ability()
self.dexterity = self._random_ability()
self.constitution = self._random_ability()
self.intelligence = self._random_ability()
self.wisdom = self._random_ability()
self.charisma = self._random_ability()
# 物理属性(仅用于角色扮演目的)
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
face = dice.roll_random_table("1d20", chargen_tables["face"])
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
background = dice.roll_random_table("1d20", chargen_tables["background"])
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
self.desc = (
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
f" and {clothing} clothing. You were a {background.title()}, but you were"
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
f" {alignment} alignment."
)
#
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
self.xp = 0
self.level = 1
# 随机装备
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
self.backpack = [
"ration",
"ration",
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
]
这里我们遵循_Knave_规则书来随机化能力、描述和装备。dice.roll()
和dice.roll_random_table
方法现在变得非常有用!这里的一切都应该很容易理解。
与基础_Knave_的主要区别在于,我们制作了一个“起始武器”的表(在Knave中你可以选择你喜欢的任何东西)。
我们还初始化了.ability_changes = 0
。Knave只允许我们交换两个能力的值_一次_。我们将用它来知道是否已经完成。
6.3.1. 显示角色纸¶
现在我们有了临时角色纸,我们应该让它易于可视化。
# 在 mygame/evadventure/chargen.py 中
_TEMP_SHEET = """
{name}
STR +{strength}
DEX +{dexterity}
CON +{constitution}
INT +{intelligence}
WIS +{wisdom}
CHA +{charisma}
{description}
你的物品:
{equipment}
"""
class TemporaryCharacterSheet:
# ...
def show_sheet(self):
equipment = (
str(item)
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
if item
)
return _TEMP_SHEET.format(
name=self.name,
strength=self.strength,
dexterity=self.dexterity,
constitution=self.constitution,
intelligence=self.intelligence,
wisdom=self.wisdom,
charisma=self.charisma,
description=self.desc,
equipment=", ".join(equipment),
)
新的show_sheet
方法从临时角色纸中收集数据,并以漂亮的形式返回它。制作一个像_TEMP_SHEET
这样的“模板”字符串可以让你在以后想要更改外观时更容易更改内容。
6.3.2. 应用角色¶
一旦我们对角色满意,我们需要用我们选择的统计数据实际创建它。这有点复杂。
# 在 mygame/evadventure/chargen.py 中
# ...
from .characters import EvAdventureCharacter
from evennia import create_object
from evennia.prototypes.spawner import spawn
class TemporaryCharacterSheet:
# ...
def apply(self):
# 用给定的能力创建角色对象
new_character = create_object(
EvAdventureCharacter,
key=self.name,
attrs=(
("strength", self.strength),
("dexterity", self.dexterity),
("constitution", self.constitution),
("intelligence", self.intelligence),
("wisdom", self.wisdom),
("charisma", self.wisdom),
("hp", self.hp),
("hp_max", self.hp_max),
("desc", self.desc),
),
)
# 生成装备(在它工作之前需要创建原型)
if self.weapon:
weapon = spawn(self.weapon)
new_character.equipment.move(weapon)
if self.shield:
shield = spawn(self.shield)
new_character.equipment.move(shield)
if self.armor:
armor = spawn(self.armor)
new_character.equipment.move(armor)
if self.helmet:
helmet = spawn(self.helmet)
new_character.equipment.move(helmet)
for item in self.backpack:
item = spawn(item)
new_character.equipment.store(item)
return new_character
我们使用create_object
创建一个新的EvAdventureCharacter
。我们将所有相关数据从临时角色纸中传入。这是这些成为实际角色的时候。
每件装备都是一个独立的对象。我们将在这里假设所有游戏物品都被定义为原型,其键为其名称,如“剑”、“锁子甲”等。
我们实际上还没有创建这些原型,所以现在我们需要假设它们在那里。一旦一件装备被生成,我们确保将其移动到我们在装备课程中创建的EquipmentHandler
中。
6.5. 主节点:选择要做的事情¶
这是第一个菜单节点。它将作为一个中心枢纽,可以从中选择不同的操作。
# 在 mygame/evadventure/chargen.py 中
# ...
# 在模块末尾,但在`start_chargen`函数之前
def node_chargen(caller, raw_string, **kwargs):
tmp_character = kwargs["tmp_character"]
text = tmp_character.show_sheet()
options = [
{
"desc": "更改你的名字",
"goto": ("node_change_name", kwargs)
}
]
if tmp_character.ability_changes <= 0:
options.append(
{
"desc": "交换两个能力值(一次)",
"goto": ("node_swap_abilities", kwargs),
}
)
options.append(
{
"desc": "接受并创建角色",
"goto": ("node_apply_character", kwargs)
},
)
return text, options
# ...
这里有很多要解析的内容!在Evennia中,命名节点函数为node_*
是惯例。虽然不是必需的,但它有助于你跟踪什么是节点,什么不是。
每个菜单节点都应该接受caller, raw_string, **kwargs
作为参数。这里的caller
是你传入EvMenu
调用的caller
。raw_string
是用户为了_进入此节点_而给出的输入,因此当前为空。**kwargs
是传入EvMenu
的所有额外关键字参数。它们也可以在节点之间传递。在这种情况下,我们将关键字tmp_character
传递给EvMenu
。我们现在在节点中拥有临时角色纸!
请注意,上面我们使用
startnode="node_chargen"
和元组startnode_input=("", {"tmp_character": tmp_character})
创建了菜单。假设我们将上述函数注册为节点"node_chargen"
,它将首先被调用为node_chargen(caller, "", tmp_character=tmp_character)
(EvMenu会自行添加caller
)。这是我们在菜单启动时将外部数据传入菜单的一种方式。
一个EvMenu
节点必须始终返回两样东西——text
和options
。text
是用户在查看此节点时将看到的内容。options
是,从这里开始到其他地方应该呈现的选项。
对于文本,我们只是获取临时角色纸的漂亮打印。单个选项被定义为一个像这样的dict
:
{
"key": ("name". "alias1", "alias2", ...), # 如果跳过,则自动显示一个数字
"desc": "描述选择选项时会发生什么的文本",
"goto": ("节点名称或可调用对象", kwargs_to_pass_into_next_node_or_callable)
}
多个选项字典以列表或元组返回。理解goto
选项键很重要。它的工作是直接指向另一个节点(通过给出其名称),或指向一个Python可调用对象(如函数)然后返回该名称。你还可以传递kwargs(作为字典)。这将在可调用对象或下一个节点中作为**kwargs
提供。
虽然一个选项可以有一个key
,但你也可以跳过它,只得到一个运行的数字。
在我们的node_chargen
节点中,我们通过名称指向三个节点:node_change_name
、node_swap_abilities
和node_apply_character
。我们还确保将kwargs
传递给每个节点,因为其中包含我们的临时角色纸。
这些选项中的中间选项仅在我们尚未交换两个能力时出现——为了知道这一点,我们检查.ability_changes
属性以确保它仍然是0。
6.6. 节点:更改你的名字¶
如果你在node_chargen
中选择更改名字,这就是你到达的地方。
# 在 mygame/evadventure/chargen.py 中
# ...
# 在上一个节点之后
def _update_name(caller, raw_string, **kwargs):
"""
由下面的node_change_name使用,以检查用户输入的内容
并在适当时更新名称。
"""
if raw_string:
tmp_character = kwargs["tmp_character"]
tmp_character.name = raw_string.lower().capitalize()
return "node_chargen", kwargs
def node_change_name(caller, raw_string, **kwargs):
"""
更改角色的随机名称。
"""
tmp_character = kwargs["tmp_character"]
text = (
f"你当前的名字是 |w{tmp_character.name}|n。"
"输入一个新名字或留空以中止。"
)
options = {
"key": "_default",
"goto": (_update_name, kwargs)
}
return text, options
这里有两个函数——菜单节点本身(node_change_name
)和一个帮助_goto_function_(_update_name
)来处理用户的输入。
对于(单个)选项,我们使用一个名为_default
的特殊key
。这使得这个选项成为一个catch-all:如果用户输入的内容与任何其他选项不匹配,这就是将使用的选项。由于我们在这里没有其他选项,所以无论用户输入什么,我们都会始终使用此选项。
还要注意,选项的goto
部分指向_update_name
可调用对象,而不是节点的名称。我们需要继续将kwargs
传递给它!
当用户在此节点上输入任何内容时,将调用_update_name
可调用对象。它具有与节点相同的参数,但它不是一个节点——我们将仅用于_找出_下一个要去的节点。
在_update_name
中,我们现在有一个raw_string
参数的用途——这就是用户在上一个节点上写的内容,记得吗?这现在要么是一个空字符串(意味着忽略它),要么是角色的新名称。
像_update_name
这样的goto函数必须返回要使用的下一个节点的名称。它还可以选择性地返回要传递给该节点的kwargs
——我们希望始终这样做,以便不会丢失我们的临时角色纸。在这里,我们将始终返回到node_chargen
。
提示:如果从goto可调用对象返回
None
,你将始终返回到你所在的最后一个节点。
6.7. 节点:交换能力¶
你通过从node_chargen
节点选择第二个选项来到这里。
# 在 mygame/evadventure/chargen.py 中
# ...
# 在上一个节点之后
_ABILITIES = {
"STR": "strength",
"DEX": "dexterity",
"CON": "constitution",
"INT": "intelligence",
"WIS": "wisdom",
"CHA": "charisma",
}
def _swap_abilities(caller, raw_string, **kwargs):
"""
由node_swap_abilities使用以解析用户的输入并交换能力值。
"""
if raw_string:
abi1, *abi2 = raw_string.split(" ", 1)
if not abi2:
caller.msg("这看起来不对。")
return None, kwargs
abi2 = abi2[0]
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
caller.msg("不是熟悉的能力集。")
return None, kwargs
# 看起来不错 = 交换值。我们需要将STR转换为strength等
tmp_character = kwargs["tmp_character"]
abi1 = _ABILITIES[abi1]
abi2 = _ABILITIES[abi2]
abival1 = getattr(tmp_character, abi1)
abival2 = getattr(tmp_character, abi2)
setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1
return "node_chargen", kwargs
def node_swap_abilities(caller, raw_string, **kwargs):
"""
允许交换两个能力的值,一次。
"""
tmp_character = kwargs["tmp_character"]
text = f"""
你当前的能力:
STR +{tmp_character.strength}
DEX +{tmp_character.dexterity}
CON +{tmp_character.constitution}
INT +{tmp_character.intelligence}
WIS +{tmp_character.wisdom}
CHA +{tmp_character.charisma}
你可以交换两个能力的值。
你只能这样做一次,所以请谨慎选择!
要交换例如STR和INT的值,请输入|wSTR INT|n。留空以中止。
"""
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
return text, options
这是更多的代码,但逻辑是相同的——我们有一个节点(node_swap_abilities
)和一个goto可调用帮助程序(_swap_abilities
)。我们捕获用户在节点上输入的所有内容(例如WIS CON
)并将其传递给帮助程序。
在_swap_abilities
中,我们需要分析用户的raw_string
以查看他们想要做什么。
帮助程序中的大多数代码都是验证用户没有输入无意义的内容。如果他们这样做了,我们使用caller.msg()
告诉他们,然后返回None, kwargs
,这将重新运行相同的节点(名称选择)。
由于我们希望用户能够输入“CON”而不是更长的“constitution”,我们需要一个映射_ABILITIES
来轻松地在两者之间转换(它存储为临时角色纸上的consitution
)。一旦我们知道他们想要交换哪些能力,我们就这样做并增加.ability_changes
计数器。这意味着此选项将不再从主节点中可用。
最后,我们再次返回到node_chargen
。
6.8. 节点:创建角色¶
我们通过选择完成角色生成的选项从主节点进入这里。
node_apply_character(caller, raw_string, **kwargs):
"""
结束角色生成并创建角色。我们还将控制它。
"""
tmp_character = kwargs["tmp_character"]
new_character = tmp_character.apply(caller)
caller.account.add_character(new_character)
text = "角色已创建!"
return text, None
进入节点时,我们将使用临时角色纸的.apply
方法创建一个包含所有装备的新角色。
这是一个_结束节点_,因为它返回None
而不是选项。之后,菜单将退出。我们将回到默认的角色选择屏幕。该屏幕上显示的角色是_playable_characters
属性中列出的角色,因此我们还需要将新角色添加到其中。
6.9. 将节点连接在一起¶
def start_chargen(caller, session=None):
"""
这是从命令启动角色生成的起点。
"""
menutree = { # <----- 现在可以添加这个!
"node_chargen": node_chargen,
"node_change_name": node_change_name,
"node_swap_abilities": node_swap_abilities,
"node_apply_character": node_apply_character,
}
# 这将生成角色的所有随机组件
tmp_character = TemporaryCharacterSheet()
EvMenu(
caller,
menutree,
session=session,
startnode="node_chargen", # <-- 确保已设置!
startnode_input=("", {"tmp_character": tmp_character}),
)
现在我们有了所有的节点,我们将它们添加到之前留空的menutree
中。我们只添加节点,而不是goto帮助程序!我们在menutree
字典中设置的键是我们应该用来从菜单内部指向节点的名称(我们已经这样做了)。
我们还添加了一个关键字参数startnode
,指向node_chargen
节点。这告诉EvMenu在菜单启动时首先跳转到该节点。
6.10. 结论¶
本课教我们如何使用EvMenu
制作交互式角色生成器。在一个比_Knave_更复杂的RPG中,菜单会更大更复杂,但相同的原则适用。
结合之前的课程,我们现在已经完成了玩家角色的基本内容——他们如何存储他们的统计数据,如何处理他们的装备以及如何创建他们。
在下一课中,我们将讨论EvAdventure _Rooms_的工作原理。